@pinepaper.studio/mcp-server 1.6.17 → 1.6.19
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 +189 -10
- package/README.pt-BR.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/agent/export-manager.js +2 -2
- package/dist/agent/session-manager.d.ts +8 -0
- package/dist/agent/session-manager.d.ts.map +1 -1
- package/dist/agent/session-manager.js +17 -2
- package/dist/agent/session-manager.js.map +1 -1
- package/dist/assets/asset-manager.d.ts +28 -1
- package/dist/assets/asset-manager.d.ts.map +1 -1
- package/dist/assets/asset-manager.js +65 -25
- package/dist/assets/asset-manager.js.map +1 -1
- package/dist/assets/index.d.ts +1 -3
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +1 -3
- package/dist/assets/index.js.map +1 -1
- package/dist/assets/repositories/iconify.js +1 -1
- package/dist/assets/repositories/iconify.js.map +1 -1
- package/dist/assets/repositories/openclipart.js +1 -1
- package/dist/assets/repositories/openclipart.js.map +1 -1
- package/dist/assets/repositories/svgrepo.js +2 -2
- package/dist/assets/repositories/svgrepo.js.map +1 -1
- package/dist/assets/types.d.ts +2 -2
- package/dist/assets/types.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.d.ts +86 -1
- package/dist/browser/puppeteer-controller.d.ts.map +1 -1
- package/dist/browser/puppeteer-controller.js +274 -6
- package/dist/browser/puppeteer-controller.js.map +1 -1
- package/dist/build-info.json +2 -2
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/design/design-systems.d.ts +2 -0
- package/dist/design/design-systems.d.ts.map +1 -1
- package/dist/design/design-systems.js +2 -0
- package/dist/design/design-systems.js.map +1 -1
- package/dist/execution/canvas-state.d.ts.map +1 -1
- package/dist/execution/canvas-state.js +19 -43
- package/dist/execution/canvas-state.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +94 -16
- package/dist/index.js.map +1 -1
- package/dist/ontology/design-graph.js +2 -2
- package/dist/ontology/design-graph.js.map +1 -1
- package/dist/ontology/ontology-validator.d.ts.map +1 -1
- package/dist/ontology/ontology-validator.js +9 -5
- package/dist/ontology/ontology-validator.js.map +1 -1
- package/dist/ontology/upstream-vocabulary.d.ts +2 -2
- package/dist/ontology/upstream-vocabulary.js +2 -2
- package/dist/ontology/validator.js +1 -1
- package/dist/ontology/validator.js.map +1 -1
- package/dist/tools/compact-descriptions.d.ts.map +1 -1
- package/dist/tools/compact-descriptions.js +15 -14
- 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 +556 -157
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/engine-lists.d.ts +35 -0
- package/dist/tools/engine-lists.d.ts.map +1 -0
- package/dist/tools/engine-lists.js +186 -0
- package/dist/tools/engine-lists.js.map +1 -0
- package/dist/tools/engine-methods.d.ts.map +1 -1
- package/dist/tools/engine-methods.js +18 -1
- 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 +66 -9
- package/dist/tools/engine-surface.js.map +1 -1
- package/dist/tools/handlers/brand-intake.d.ts +95 -0
- package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
- package/dist/tools/handlers/brand-intake.js +242 -0
- package/dist/tools/handlers/brand-intake.js.map +1 -0
- package/dist/tools/handlers/camera.js +8 -8
- package/dist/tools/handlers/camera.js.map +1 -1
- package/dist/tools/handlers/export.d.ts.map +1 -1
- package/dist/tools/handlers/export.js +77 -5
- package/dist/tools/handlers/export.js.map +1 -1
- package/dist/tools/handlers/font.js +10 -10
- package/dist/tools/handlers/font.js.map +1 -1
- package/dist/tools/handlers/generate.d.ts +12 -12
- package/dist/tools/handlers/maps.d.ts.map +1 -1
- package/dist/tools/handlers/maps.js +23 -3
- package/dist/tools/handlers/maps.js.map +1 -1
- package/dist/tools/handlers.d.ts +44 -11
- package/dist/tools/handlers.d.ts.map +1 -1
- package/dist/tools/handlers.js +462 -43
- package/dist/tools/handlers.js.map +1 -1
- package/dist/tools/minimal-descriptions.d.ts.map +1 -1
- package/dist/tools/minimal-descriptions.js +36 -29
- package/dist/tools/minimal-descriptions.js.map +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 +5 -2
- package/dist/tools/shape-params.js.map +1 -1
- package/dist/tools/stick-vocabulary.d.ts +1 -1
- package/dist/tools/stick-vocabulary.js +1 -1
- package/dist/tools/toolkits.d.ts.map +1 -1
- package/dist/tools/toolkits.js +5 -0
- package/dist/tools/toolkits.js.map +1 -1
- package/dist/types/code-generator.d.ts +96 -4
- package/dist/types/code-generator.d.ts.map +1 -1
- package/dist/types/code-generator.js +1265 -277
- package/dist/types/code-generator.js.map +1 -1
- package/dist/types/schemas.d.ts +4181 -1757
- package/dist/types/schemas.d.ts.map +1 -1
- package/dist/types/schemas.js +378 -84
- package/dist/types/schemas.js.map +1 -1
- package/dist/utils/mcp-log.d.ts +16 -0
- package/dist/utils/mcp-log.d.ts.map +1 -0
- package/dist/utils/mcp-log.js +10 -0
- package/dist/utils/mcp-log.js.map +1 -0
- package/dist/utils/progress.d.ts +28 -0
- package/dist/utils/progress.d.ts.map +1 -0
- package/dist/utils/progress.js +24 -0
- package/dist/utils/progress.js.map +1 -0
- package/dist/vendor/design/design-style.js +1 -1
- package/manifest.json +22 -2
- package/package.json +7 -4
package/dist/tools/handlers.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Supports i18n for localized error and success messages.
|
|
6
6
|
* Supports browser execution via Puppeteer.
|
|
7
7
|
*/
|
|
8
|
-
import { codeGenerator, generateQueryMutationsCode } from '../types/code-generator.js';
|
|
9
|
-
import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
|
|
8
|
+
import { codeGenerator, generateQueryMutationsCode, SHEET_RUBRIC } from '../types/code-generator.js';
|
|
9
|
+
import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, RelightInputSchema, 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,
|
|
10
10
|
// Diagram schemas
|
|
11
11
|
CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
|
|
12
12
|
// Agent flow mode schemas
|
|
@@ -27,7 +27,7 @@ ImportImageInputSchema,
|
|
|
27
27
|
// Scene management schemas
|
|
28
28
|
ManageScenesInputSchema, ScenePlaybackInputSchema, SceneGraphInputSchema, SequenceInputSchema, StaggerInputSchema, FlipInputSchema,
|
|
29
29
|
// New consolidated tool schemas
|
|
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';
|
|
30
|
+
SelectionInputSchema, TransformInputSchema, HistoryInputSchema, ImageFilterInputSchema, LassoInputSchema, CutoutStyleInputSchema, PrecompInputSchema, BackgroundInputSchema, QueryInputSchema, QueryCapabilitiesInputSchema, DeformInputSchema, SpriteSheetInputSchema, InteractionInputSchema, ExportWidgetInputSchema, ExportWidgetHtmlInputSchema, CreateChartInputSchema, MagicInputSchema, PhysicsInputSchema, MeasurementInputSchema, PlaceOnSurfaceInputSchema, StyledSceneInputSchema, ErrorCodes, } from '../types/schemas.js';
|
|
31
31
|
import { ZodError, z } from 'zod';
|
|
32
32
|
import { writeFile, mkdir, appendFile, unlink, readFile, stat } from 'node:fs/promises';
|
|
33
33
|
import { tmpdir } from 'node:os';
|
|
@@ -37,6 +37,7 @@ import { getBrowserController, resetBrowserController, } from '../browser/puppet
|
|
|
37
37
|
import { sameStudioTarget, validateStudioUrl } from '../browser/url-target.js';
|
|
38
38
|
import { getPerformanceTracker } from '../metrics/index.js';
|
|
39
39
|
import { formatErrorContext, captureCanvasState } from '../execution/index.js';
|
|
40
|
+
import { FILTER_PARAM_RANGES } from './engine-lists.js';
|
|
40
41
|
import { getSessionManager } from '../agent/session-manager.js';
|
|
41
42
|
import { vocabularyHintForPath, validateBatchVocabulary, detectBatchPropertyTypos } from '../ontology/hints.js';
|
|
42
43
|
import { cameraHandlers } from './handlers/camera.js';
|
|
@@ -85,13 +86,19 @@ export function getScreenshotMode() {
|
|
|
85
86
|
// =============================================================================
|
|
86
87
|
// EXPORT FILE SAVE HELPERS
|
|
87
88
|
// =============================================================================
|
|
88
|
-
function getExportDir() {
|
|
89
|
+
export function getExportDir() {
|
|
89
90
|
return process.env.PINEPAPER_EXPORT_DIR || join(tmpdir(), 'pinepaper-exports');
|
|
90
91
|
}
|
|
91
92
|
// wav joins them: a minute of 48kHz 16-bit is ~5.8 MB of base64 and the ten
|
|
92
93
|
// minutes the schema allows is ~77 MB. Deliverable across the bridge, useless
|
|
93
94
|
// pasted into a response.
|
|
94
|
-
|
|
95
|
+
// Stills join them too. The threshold below is in CHARACTERS of base64, and a
|
|
96
|
+
// 59 KB 1920x1080 PNG is ~79k of them: under the threshold, so it came back
|
|
97
|
+
// inline, and over the token limit of the client reading it (a Halloween
|
|
98
|
+
// look-frame session, 2026-09-29), while bigger PNGs saved to a file. Same
|
|
99
|
+
// format, two shapes, decided by file size. An image is never useful as
|
|
100
|
+
// base64 in a text result, so png/jpg/webp always save and name the path.
|
|
101
|
+
export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'apng', 'pdf', 'wav', 'srt', 'vtt', 'scc', 'html5-ad', 'playable', 'png', 'jpg', 'webp']);
|
|
95
102
|
// 500_000 was chosen against the bridge's limits, not the CALLER's. A pilot
|
|
96
103
|
// session hit a 263K-character end_job result — comfortably under this, so it
|
|
97
104
|
// was returned inline, and over the tool-result limit of the client reading
|
|
@@ -554,10 +561,37 @@ export function getExecutionMode() {
|
|
|
554
561
|
// Default to 'puppeteer' for live browser execution
|
|
555
562
|
return 'puppeteer';
|
|
556
563
|
}
|
|
564
|
+
/** Where a generated script travels for a host to record, beside the agent-visible text. */
|
|
565
|
+
/** Relation types registered with register_custom_relation in this server session (gate D2). */
|
|
566
|
+
export const CUSTOM_RELATION_NAMES = new Set();
|
|
567
|
+
/**
|
|
568
|
+
* Tools whose browser work legitimately outlasts the governor's 10 s default.
|
|
569
|
+
* The first detection call downloads and initialises a model, which on a cold
|
|
570
|
+
* page took longer than 10 s and died as a timeout (gate C10, 1.6.19).
|
|
571
|
+
*/
|
|
572
|
+
const LONG_GOVERNOR_BUDGET_MS = {
|
|
573
|
+
pinepaper_detect_objects: 120_000,
|
|
574
|
+
pinepaper_extract_object: 120_000,
|
|
575
|
+
};
|
|
576
|
+
export const CODE_META_KEY = 'pinepaper.studio/code';
|
|
557
577
|
// =============================================================================
|
|
558
578
|
// RESULT HELPERS
|
|
559
579
|
// =============================================================================
|
|
560
|
-
function successResult(code, description) {
|
|
580
|
+
function successResult(code, description, deferred = false) {
|
|
581
|
+
if (deferred) {
|
|
582
|
+
return {
|
|
583
|
+
content: [{
|
|
584
|
+
type: 'text',
|
|
585
|
+
text: JSON.stringify({
|
|
586
|
+
success: true,
|
|
587
|
+
status: 'recorded',
|
|
588
|
+
step: description || 'canvas step',
|
|
589
|
+
note: 'Recorded on the scene. It runs in the studio when the scene renders or exports, not now; keep building, then render.',
|
|
590
|
+
}, null, 2),
|
|
591
|
+
}],
|
|
592
|
+
_meta: { [CODE_META_KEY]: code },
|
|
593
|
+
};
|
|
594
|
+
}
|
|
561
595
|
const content = [
|
|
562
596
|
{
|
|
563
597
|
type: 'text',
|
|
@@ -570,7 +604,7 @@ function successResult(code, description) {
|
|
|
570
604
|
text: `\n${description}`,
|
|
571
605
|
});
|
|
572
606
|
}
|
|
573
|
-
return { content };
|
|
607
|
+
return { content, _meta: { [CODE_META_KEY]: code } };
|
|
574
608
|
}
|
|
575
609
|
/**
|
|
576
610
|
* A plain data answer.
|
|
@@ -583,6 +617,45 @@ function successResult(code, description) {
|
|
|
583
617
|
function dataResult(value) {
|
|
584
618
|
return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
|
|
585
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Where a call's time went (O1, per-request trace). In the result's _meta, the
|
|
622
|
+
* spec's slot for metadata, under one namespaced key, so it adds nothing to
|
|
623
|
+
* the text a model reads. browserMs includes the screenshot when one was taken.
|
|
624
|
+
* index.ts adds toolMs, the whole call, for every tool.
|
|
625
|
+
*/
|
|
626
|
+
export const TIMING_META_KEY = 'pinepaper.studio/timing';
|
|
627
|
+
export function withTiming(out, t) {
|
|
628
|
+
const meta = (out._meta ?? {});
|
|
629
|
+
const prev = (meta[TIMING_META_KEY] ?? {});
|
|
630
|
+
return { ...out, _meta: { ...meta, [TIMING_META_KEY]: { ...prev, ...t } } };
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* A call that only succeeded after the browser session was recovered says so,
|
|
634
|
+
* in the result: the controller has reported `recovered` / `canvasReset` for
|
|
635
|
+
* a while, but nothing read them, so an agent kept using item ids from a
|
|
636
|
+
* scene that was gone (M6).
|
|
637
|
+
*/
|
|
638
|
+
function withSessionNote(out, r) {
|
|
639
|
+
if (!r.recovered)
|
|
640
|
+
return out;
|
|
641
|
+
const note = r.relaunched
|
|
642
|
+
? `The browser session had been lost; the browser was relaunched and this call ran on an EMPTY canvas${r.canvasSizeRestored ? ` (size restored to ${r.canvasSizeRestored.width}x${r.canvasSizeRestored.height})` : ' at the default size'}. Item ids from before are gone: rebuild the scene.`
|
|
643
|
+
: r.canvasReset
|
|
644
|
+
? 'The studio page had reloaded or navigated away, and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
|
|
645
|
+
: 'The page connection was re-established; the scene survived.';
|
|
646
|
+
const first = out.content[0];
|
|
647
|
+
if (first && first.type === 'text') {
|
|
648
|
+
try {
|
|
649
|
+
const obj = JSON.parse(first.text);
|
|
650
|
+
obj.session = { recovered: true, ...(r.relaunched ? { relaunched: true } : {}), ...(r.canvasReset !== undefined ? { canvasReset: r.canvasReset } : {}), note };
|
|
651
|
+
return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
return { ...out, content: [{ type: 'text', text: `${note}\n\n${first.text}` }, ...out.content.slice(1)] };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
586
659
|
function executedResult(code, result, screenshot, description, report) {
|
|
587
660
|
const resultObj = { success: true, result };
|
|
588
661
|
if (description)
|
|
@@ -740,6 +813,204 @@ function handleValidationError(error, i18n) {
|
|
|
740
813
|
*
|
|
741
814
|
* Default is 'on_request' for optimal performance per best practices.
|
|
742
815
|
*/
|
|
816
|
+
/** Page code: remember the id of every item on the canvas. */
|
|
817
|
+
const MARK_ITEMS_BEFORE_RUN = `(function () {
|
|
818
|
+
if (typeof paper === 'undefined' || !paper.project) return { ok: false };
|
|
819
|
+
const ids = new Set();
|
|
820
|
+
paper.project.getItems({}).forEach(function (i) { ids.add(i.id); });
|
|
821
|
+
window.__ppPreRunIds = ids;
|
|
822
|
+
return { ok: true };
|
|
823
|
+
})();`;
|
|
824
|
+
/** Page code: remove every item that was not there at the mark, outermost first. */
|
|
825
|
+
const REMOVE_ITEMS_CREATED_SINCE_MARK = `(function () {
|
|
826
|
+
const pre = window.__ppPreRunIds;
|
|
827
|
+
window.__ppPreRunIds = null;
|
|
828
|
+
if (!pre || typeof paper === 'undefined' || !paper.project) return { ok: false };
|
|
829
|
+
const fresh = paper.project.getItems({}).filter(function (i) { return !pre.has(i.id); });
|
|
830
|
+
const freshIds = new Set(fresh.map(function (i) { return i.id; }));
|
|
831
|
+
const tops = fresh.filter(function (i) { return !(i.parent && freshIds.has(i.parent.id)); });
|
|
832
|
+
let removed = 0;
|
|
833
|
+
tops.forEach(function (it) {
|
|
834
|
+
try { if (typeof app !== 'undefined' && typeof app.deleteItem === 'function' && app.deleteItem(it)) { removed++; return; } } catch (e) {}
|
|
835
|
+
try { if (it.parent) { it.remove(); removed++; } } catch (e) {}
|
|
836
|
+
});
|
|
837
|
+
const left = paper.project.getItems({}).filter(function (i) { return !pre.has(i.id); }).length;
|
|
838
|
+
try { if (paper.view) paper.view.update(); } catch (e) {}
|
|
839
|
+
return { ok: left === 0, removed: removed, left: left };
|
|
840
|
+
})();`;
|
|
841
|
+
/**
|
|
842
|
+
* Record which canvas items exist before agent-written code runs, so a failed
|
|
843
|
+
* run's leftovers can be told apart from the scene it started with. Returns
|
|
844
|
+
* false when the page could not be marked (the rollback is then skipped and
|
|
845
|
+
* the failure note says nothing about the scene it cannot vouch for).
|
|
846
|
+
*/
|
|
847
|
+
async function markItemsBeforeRun(controller) {
|
|
848
|
+
try {
|
|
849
|
+
const r = await controller.executeCode(MARK_ITEMS_BEFORE_RUN, false, { bypassGovernor: true });
|
|
850
|
+
return !!(r.success && r.result?.ok);
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Remove what agent-written code created before it failed, and say so.
|
|
858
|
+
*
|
|
859
|
+
* A run stopped by the loop deadline after drawing 42 strokes left them on
|
|
860
|
+
* the canvas: a half-painted scene. Undo cannot fix that. The governor's one
|
|
861
|
+
* history entry only covers mutations that announce themselves, and raw
|
|
862
|
+
* `new paper.Path(...)` does not (measured: report.mutated false, 30 paths
|
|
863
|
+
* left behind). So the rollback is by identity instead: every item that was
|
|
864
|
+
* not on the canvas before the call goes, through app.deleteItem where the
|
|
865
|
+
* engine knows the item (registry, trackers) and item.remove() otherwise.
|
|
866
|
+
* Edits to items that already existed are NOT reverted; the note says so.
|
|
867
|
+
*/
|
|
868
|
+
export async function customCodeRollback(controller, error) {
|
|
869
|
+
let rolledBack = false;
|
|
870
|
+
let removed = 0;
|
|
871
|
+
let left = -1;
|
|
872
|
+
try {
|
|
873
|
+
const r = await controller.executeCode(REMOVE_ITEMS_CREATED_SINCE_MARK, false, { bypassGovernor: true });
|
|
874
|
+
const v = (r.result || {});
|
|
875
|
+
rolledBack = !!(r.success && v.ok);
|
|
876
|
+
removed = v.removed ?? 0;
|
|
877
|
+
left = typeof v.left === 'number' ? v.left : -1;
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
rolledBack = false;
|
|
881
|
+
}
|
|
882
|
+
const scene = rolledBack
|
|
883
|
+
? (removed > 0
|
|
884
|
+
? `The ${removed} item(s) this call created before it stopped were removed. Changes it made to items that already existed are not reverted.`
|
|
885
|
+
: 'This call created no items before it stopped. Changes it made to items that already existed are not reverted.')
|
|
886
|
+
: `Items this call created could not all be removed${left > 0 ? ` (${left} remain)` : ''}: check the canvas before continuing.`;
|
|
887
|
+
const how = /Synchronous execution exceeded|Loop budget exceeded/i.test(error || '')
|
|
888
|
+
? ' This is execute_custom_code\'s per-call guard, not a limit on the scene: its loops may run about 4 s (and 2,000,000 iterations per loop) so the studio tab stays responsive. Split the work across several calls, each of which gets a fresh budget. For example, paint 10 strokes per call instead of 40 in one.'
|
|
889
|
+
: '';
|
|
890
|
+
return { rolledBack, note: scene + how };
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* pinepaper_brand_kit from_url: the product's real brand, read from its own
|
|
894
|
+
* site (see handlers/brand-intake.ts). Runs in the MCP server's browser, in an
|
|
895
|
+
* isolated context, so code mode cannot do it and says so.
|
|
896
|
+
*/
|
|
897
|
+
async function brandFromUrl(url, shots, options) {
|
|
898
|
+
const effectiveExecMode = options.executionMode ?? getExecutionMode();
|
|
899
|
+
if (effectiveExecMode === 'code' || !options.executeInBrowser) {
|
|
900
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, 'brand_kit from_url reads the site in the MCP server\'s browser, so it needs browser mode; it cannot run as generated code.', {}, { toolName: 'pinepaper_brand_kit' });
|
|
901
|
+
}
|
|
902
|
+
const controller = options.browserController || getBrowserController();
|
|
903
|
+
if (!controller.connected) {
|
|
904
|
+
try {
|
|
905
|
+
await controller.connect();
|
|
906
|
+
}
|
|
907
|
+
catch (e) {
|
|
908
|
+
return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, `could not start the browser: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const host = (() => { try {
|
|
912
|
+
return new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '_');
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
return 'site';
|
|
916
|
+
} })();
|
|
917
|
+
const outDir = join(getExportDir(), `brand-${host}-${Date.now()}`);
|
|
918
|
+
try {
|
|
919
|
+
const { intakeBrand } = await import('./handlers/brand-intake.js');
|
|
920
|
+
const r = await controller
|
|
921
|
+
.withIsolatedPage((page) => intakeBrand(page, url, outDir, shots));
|
|
922
|
+
const text = [
|
|
923
|
+
`Brand read from ${url}. Files in ${outDir}:`,
|
|
924
|
+
...r.screenshots.map((f) => `- screenshot: ${f}`),
|
|
925
|
+
r.logo ? `- logo: ${r.logo.file} (from ${r.logo.source})` : '- logo: none found in the header, icons or og:image',
|
|
926
|
+
'',
|
|
927
|
+
'Review the proposed kit against the screenshots, then apply it with pinepaper_brand_kit { action: "apply", kit } (or "plan" first). Import the logo or a screenshot with pinepaper_import_image { url: <file> }.',
|
|
928
|
+
'',
|
|
929
|
+
JSON.stringify({ success: true, ...r }, null, 2),
|
|
930
|
+
].join('\n');
|
|
931
|
+
return { content: [{ type: 'text', text }] };
|
|
932
|
+
}
|
|
933
|
+
catch (e) {
|
|
934
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, `could not read ${url}: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* If an executed result carries `data` (a data URL, or JSON/text), write it to
|
|
939
|
+
* a file in the export dir and replace it with filePath + bytes.
|
|
940
|
+
*/
|
|
941
|
+
async function writeResultDataToFile(out, ext) {
|
|
942
|
+
const first = out.content?.[0];
|
|
943
|
+
if (out.isError || !first || first.type !== 'text')
|
|
944
|
+
return out;
|
|
945
|
+
let obj;
|
|
946
|
+
try {
|
|
947
|
+
obj = JSON.parse(first.text);
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
return out;
|
|
951
|
+
}
|
|
952
|
+
const r = obj.result;
|
|
953
|
+
if (!r || r.data === undefined || r.data === null)
|
|
954
|
+
return out;
|
|
955
|
+
let bytes;
|
|
956
|
+
if (typeof r.data === 'string' && r.data.startsWith('data:'))
|
|
957
|
+
bytes = Buffer.from(r.data.slice(r.data.indexOf(',') + 1), 'base64');
|
|
958
|
+
else if (typeof r.data === 'string')
|
|
959
|
+
bytes = Buffer.from(r.data, 'utf-8');
|
|
960
|
+
else
|
|
961
|
+
bytes = Buffer.from(JSON.stringify(r.data), 'utf-8');
|
|
962
|
+
const dir = getExportDir();
|
|
963
|
+
await mkdir(dir, { recursive: true });
|
|
964
|
+
const filePath = join(dir, `pinepaper_${String(r.format ?? 'export').replace(/[^a-z0-9-]/gi, '')}_${Date.now()}.${ext}`);
|
|
965
|
+
await writeFile(filePath, bytes);
|
|
966
|
+
const { data: _d, ...rest } = r;
|
|
967
|
+
void _d;
|
|
968
|
+
obj.result = { ...rest, filePath, bytes: bytes.length };
|
|
969
|
+
return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
|
|
973
|
+
* to a file (an image is never useful inline, see ALWAYS_SAVE_FORMATS), with
|
|
974
|
+
* the rubric the agent scores it against. Code mode returns the code as usual.
|
|
975
|
+
*/
|
|
976
|
+
async function captureSheet(input, options) {
|
|
977
|
+
const code = codeGenerator.generateCaptureSheet(input);
|
|
978
|
+
const description = 'Contact sheet';
|
|
979
|
+
const effectiveExecMode = options.executionMode ?? getExecutionMode();
|
|
980
|
+
if (effectiveExecMode === 'code' || !options.executeInBrowser) {
|
|
981
|
+
return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
|
|
982
|
+
}
|
|
983
|
+
const controller = options.browserController || getBrowserController();
|
|
984
|
+
if (!controller.connected) {
|
|
985
|
+
try {
|
|
986
|
+
await controller.connect();
|
|
987
|
+
}
|
|
988
|
+
catch {
|
|
989
|
+
return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
// Frames wait for video seeks one at a time, so 48 of them can outlast the
|
|
993
|
+
// governor's 10 s default.
|
|
994
|
+
const run = await controller.executeCode(code, false, { governorTimeoutMs: 120_000 });
|
|
995
|
+
const value = (run.result ?? {});
|
|
996
|
+
if (!run.success || value.success === false || typeof value.dataUrl !== 'string') {
|
|
997
|
+
return errorResult(ErrorCodes.EXECUTION_ERROR, run.error || value.error || 'the contact sheet could not be made', { code, governorReport: run.report }, { toolName: 'pinepaper_capture_frames' });
|
|
998
|
+
}
|
|
999
|
+
const format = input.sheet?.format === 'png' ? 'png' : 'jpg';
|
|
1000
|
+
const { filePath, fileSize } = await saveExportToFile(value.dataUrl, format, 'sheet');
|
|
1001
|
+
const { dataUrl: _d, success: _s, ...facts } = value;
|
|
1002
|
+
void _d;
|
|
1003
|
+
void _s;
|
|
1004
|
+
const result = { success: true, filePath, fileSize, format, ...facts, review: SHEET_RUBRIC };
|
|
1005
|
+
return {
|
|
1006
|
+
content: [{
|
|
1007
|
+
type: 'text',
|
|
1008
|
+
text: `Contact sheet saved: ${filePath} (${(fileSize / 1024).toFixed(1)} KB, ${String(facts.tiles)} tiles).\n`
|
|
1009
|
+
+ 'Open the image and score it against review.criteria (1-10 each), then check review.checklist.\n\n'
|
|
1010
|
+
+ JSON.stringify(result, null, 2),
|
|
1011
|
+
}],
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
743
1014
|
export async function executeOrGenerate(code, description, options, toolName,
|
|
744
1015
|
/**
|
|
745
1016
|
* Values too large to live inside the generated code. Written to
|
|
@@ -801,7 +1072,7 @@ The code is ready to use. Each subsequent tool call will also generate code you
|
|
|
801
1072
|
timestamp: Date.now(),
|
|
802
1073
|
success: true,
|
|
803
1074
|
});
|
|
804
|
-
return successResult(code, description);
|
|
1075
|
+
return successResult(code, description, options.deferred === true);
|
|
805
1076
|
}
|
|
806
1077
|
const controller = browserController || getBrowserController();
|
|
807
1078
|
// Auto-connect in agent mode if not connected
|
|
@@ -880,18 +1151,33 @@ ${code}
|
|
|
880
1151
|
if (shouldTakeScreenshot) {
|
|
881
1152
|
tracker.startTimer(`${timerId}_screenshot`);
|
|
882
1153
|
}
|
|
883
|
-
|
|
1154
|
+
// Agent-written code is rolled back when it fails part-way; a tool's own
|
|
1155
|
+
// emitter is not (its verdict already describes what it left behind).
|
|
1156
|
+
const rollbackOnFailure = toolName === 'pinepaper_execute_custom_code'
|
|
1157
|
+
&& await markItemsBeforeRun(controller);
|
|
1158
|
+
const governorTimeoutMs = LONG_GOVERNOR_BUDGET_MS[toolName];
|
|
1159
|
+
const result = await controller.executeCode(code, shouldTakeScreenshot, {
|
|
1160
|
+
...(stage ? { stage } : {}),
|
|
1161
|
+
...(governorTimeoutMs ? { governorTimeoutMs } : {}),
|
|
1162
|
+
});
|
|
884
1163
|
const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
|
|
1164
|
+
// THE METRIC RECORDS THE CALL'S VERDICT, NOT THE TRANSPORT'S (gate H2,
|
|
1165
|
+
// 1.6.19). result.success only says the code RAN; an emitter that ran and
|
|
1166
|
+
// returned { success: false } is a failed call, and get_performance_metrics
|
|
1167
|
+
// reported "Success Rate: 100.0%" over exactly such a failure.
|
|
1168
|
+
const innerVerdict = result.success ? innerFailure(result.result) : null;
|
|
885
1169
|
tracker.recordMetric({
|
|
886
1170
|
toolName,
|
|
887
1171
|
phase: 'browser_execution',
|
|
888
1172
|
duration: browserDuration,
|
|
889
1173
|
timestamp: Date.now(),
|
|
890
|
-
success: result.success,
|
|
891
|
-
error: result.error,
|
|
1174
|
+
success: result.success && !innerVerdict,
|
|
1175
|
+
error: result.error || innerVerdict || undefined,
|
|
892
1176
|
});
|
|
1177
|
+
let screenshotMs;
|
|
893
1178
|
if (shouldTakeScreenshot) {
|
|
894
1179
|
const screenshotDuration = tracker.endTimer(`${timerId}_screenshot`);
|
|
1180
|
+
screenshotMs = screenshotDuration;
|
|
895
1181
|
tracker.recordMetric({
|
|
896
1182
|
toolName,
|
|
897
1183
|
phase: 'screenshot',
|
|
@@ -907,19 +1193,23 @@ ${code}
|
|
|
907
1193
|
phase: 'total',
|
|
908
1194
|
duration: totalDuration,
|
|
909
1195
|
timestamp: Date.now(),
|
|
910
|
-
success: result.success,
|
|
911
|
-
error: result.error,
|
|
1196
|
+
success: result.success && !innerVerdict,
|
|
1197
|
+
error: result.error || innerVerdict || undefined,
|
|
912
1198
|
});
|
|
1199
|
+
const timing = { browserMs: Math.round(browserDuration), ...(screenshotMs !== undefined ? { screenshotMs: Math.round(screenshotMs) } : {}) };
|
|
913
1200
|
if (!result.success) {
|
|
914
1201
|
// Capture canvas state for error context
|
|
915
1202
|
const canvasState = await captureCanvasState(controller);
|
|
916
|
-
return errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
|
|
1203
|
+
return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
|
|
917
1204
|
// errorCode surfaces the governor's structured code (PP_ITEM_BUDGET,
|
|
918
1205
|
// PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
|
|
919
|
-
{
|
|
1206
|
+
{
|
|
1207
|
+
code, errorCode: result.errorCode, governorReport: result.report,
|
|
1208
|
+
...(rollbackOnFailure ? await customCodeRollback(controller, result.error) : {}),
|
|
1209
|
+
}, {
|
|
920
1210
|
toolName,
|
|
921
1211
|
canvasState: canvasState || undefined,
|
|
922
|
-
});
|
|
1212
|
+
}), timing);
|
|
923
1213
|
}
|
|
924
1214
|
// TWO VERDICTS. `result.success` says the CODE RAN. The emitted code has its
|
|
925
1215
|
// own verdict, and almost every emitter in this repo returns
|
|
@@ -933,12 +1223,24 @@ ${code}
|
|
|
933
1223
|
// outer success and the agent kept building on a rig that did not exist.
|
|
934
1224
|
// pinepaper_agent_export got this check in 1.6.9; it belongs HERE, where
|
|
935
1225
|
// every tool inherits it.
|
|
936
|
-
const inner =
|
|
1226
|
+
const inner = innerVerdict;
|
|
937
1227
|
if (inner) {
|
|
938
1228
|
const canvasState = await captureCanvasState(controller);
|
|
939
|
-
return errorResult(ErrorCodes.EXECUTION_ERROR, inner, {
|
|
1229
|
+
return withTiming(errorResult(ErrorCodes.EXECUTION_ERROR, inner, {
|
|
1230
|
+
code, result: result.result, governorReport: result.report,
|
|
1231
|
+
...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
|
|
1232
|
+
}, { toolName, canvasState: canvasState || undefined }), timing);
|
|
1233
|
+
}
|
|
1234
|
+
// Session item counts come from the governor's own count of what this call
|
|
1235
|
+
// created (gate H3: the session stats read 0 with items on the canvas).
|
|
1236
|
+
const created = result.report?.items?.created;
|
|
1237
|
+
if (typeof created === 'number' && created > 0) {
|
|
1238
|
+
try {
|
|
1239
|
+
getSessionManager().noteItemsCreated(created);
|
|
1240
|
+
}
|
|
1241
|
+
catch { /* no session */ }
|
|
940
1242
|
}
|
|
941
|
-
return executedResult(code, result.result, result.screenshot, description, result.report);
|
|
1243
|
+
return withTiming(withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result), timing);
|
|
942
1244
|
}
|
|
943
1245
|
/**
|
|
944
1246
|
* The emitted code's own verdict, when it reported one and it was a failure.
|
|
@@ -1496,14 +1798,38 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1496
1798
|
}
|
|
1497
1799
|
case 'pinepaper_validate_scene': {
|
|
1498
1800
|
const input = ValidateSceneInputSchema.parse(args);
|
|
1801
|
+
// The validator reads { from, to, relation } on an addRelation op; the
|
|
1802
|
+
// add_relation tool's own names (source, target, relationType) are what
|
|
1803
|
+
// an agent reaches for, and the tool's example used them, so every
|
|
1804
|
+
// example op failed SOURCE_NOT_FOUND "undefined" (gate D8, 1.6.19).
|
|
1805
|
+
if (Array.isArray(input.ops)) {
|
|
1806
|
+
input.ops = input.ops.map((op) => {
|
|
1807
|
+
const o = { ...op };
|
|
1808
|
+
if (o.from === undefined && o.source !== undefined) {
|
|
1809
|
+
o.from = o.source;
|
|
1810
|
+
delete o.source;
|
|
1811
|
+
}
|
|
1812
|
+
if (o.to === undefined && o.target !== undefined) {
|
|
1813
|
+
o.to = o.target;
|
|
1814
|
+
delete o.target;
|
|
1815
|
+
}
|
|
1816
|
+
if (o.relation === undefined && o.relationType !== undefined) {
|
|
1817
|
+
o.relation = o.relationType;
|
|
1818
|
+
delete o.relationType;
|
|
1819
|
+
}
|
|
1820
|
+
return o;
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1499
1823
|
const code = codeGenerator.generateValidateScene(input);
|
|
1500
1824
|
const description = input.ops ? `Pre-validate ${input.ops.length} proposed op(s)` : 'Audit the live scene';
|
|
1501
1825
|
return executeOrGenerate(code, description, options, 'pinepaper_validate_scene');
|
|
1502
1826
|
}
|
|
1503
1827
|
case 'pinepaper_capture_frames': {
|
|
1504
1828
|
const input = CaptureFramesInputSchema.parse(args);
|
|
1829
|
+
if (input.sheet)
|
|
1830
|
+
return captureSheet(input, options);
|
|
1505
1831
|
const code = codeGenerator.generateCaptureFrames(input);
|
|
1506
|
-
const description = `Deterministic capture of ${input.times.length} frame(s)`;
|
|
1832
|
+
const description = `Deterministic capture of ${(input.times ?? []).length} frame(s)`;
|
|
1507
1833
|
return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
|
|
1508
1834
|
}
|
|
1509
1835
|
case 'pinepaper_instantiate_ontology': {
|
|
@@ -1561,8 +1887,27 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1561
1887
|
return executeOrGenerate(code, `Text effect: ${input.action}`, options, 'pinepaper_text_effect');
|
|
1562
1888
|
}
|
|
1563
1889
|
// ─── 1.6.4: agent surface for the Tier-2 engine features ───
|
|
1890
|
+
case 'pinepaper_relight': {
|
|
1891
|
+
const input = RelightInputSchema.parse(args);
|
|
1892
|
+
return executeOrGenerate(codeGenerator.generateRelight(input), `Relight: ${input.action}`, options, 'pinepaper_relight');
|
|
1893
|
+
}
|
|
1894
|
+
case 'pinepaper_shader_graph': {
|
|
1895
|
+
const input = ShaderGraphInputSchema.parse(args);
|
|
1896
|
+
return executeOrGenerate(codeGenerator.generateShaderGraph(input), `Shader graph: ${input.action}`, options, 'pinepaper_shader_graph');
|
|
1897
|
+
}
|
|
1898
|
+
case 'pinepaper_styled_scene': {
|
|
1899
|
+
const input = StyledSceneInputSchema.parse(args);
|
|
1900
|
+
return executeOrGenerate(codeGenerator.generateStyledScene(input), `Styled scene${input.style ? `: ${input.style}` : ''}`, options, 'pinepaper_styled_scene');
|
|
1901
|
+
}
|
|
1902
|
+
case 'pinepaper_place_on_surface': {
|
|
1903
|
+
const input = PlaceOnSurfaceInputSchema.parse(args);
|
|
1904
|
+
const code = codeGenerator.generatePlaceOnSurface(input);
|
|
1905
|
+
return executeOrGenerate(code, 'Place a UI onto a screen in a photo', options, 'pinepaper_place_on_surface');
|
|
1906
|
+
}
|
|
1564
1907
|
case 'pinepaper_brand_kit': {
|
|
1565
1908
|
const input = BrandKitInputSchema.parse(args);
|
|
1909
|
+
if (input.action === 'from_url')
|
|
1910
|
+
return brandFromUrl(input.url, input.screenshots ?? 3, options);
|
|
1566
1911
|
const code = codeGenerator.generateBrandKit(input);
|
|
1567
1912
|
return executeOrGenerate(code, `Brand kit: ${input.action}`, options, 'pinepaper_brand_kit');
|
|
1568
1913
|
}
|
|
@@ -1820,8 +2165,17 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1820
2165
|
// RELATION TOOLS
|
|
1821
2166
|
// -----------------------------------------------------------------------
|
|
1822
2167
|
case 'pinepaper_add_relation': {
|
|
1823
|
-
|
|
1824
|
-
|
|
2168
|
+
// A relation registered with register_custom_relation this session is
|
|
2169
|
+
// a valid type here (gate D2, 1.6.19: the enum refused the name just
|
|
2170
|
+
// registered). Only those names; an unknown one is still refused.
|
|
2171
|
+
const rt = args.relationType;
|
|
2172
|
+
// The schema's refinements check presence, not which type, so a custom
|
|
2173
|
+
// name is validated with a built-in stand-in and then put back.
|
|
2174
|
+
const isCustom = typeof rt === 'string' && CUSTOM_RELATION_NAMES.has(rt);
|
|
2175
|
+
const input = AddRelationInputSchema.parse(isCustom ? { ...args, relationType: 'orbits' } : args);
|
|
2176
|
+
if (isCustom)
|
|
2177
|
+
input.relationType = rt;
|
|
2178
|
+
const code = codeGenerator.generateAddRelation(input, isCustom);
|
|
1825
2179
|
const description = getLocalizedSuccessMessage(i18n, 'relationAdded', {
|
|
1826
2180
|
relationType: input.relationType ?? `preset:${input.presetId}`,
|
|
1827
2181
|
sourceId: input.sourceId,
|
|
@@ -1845,6 +2199,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1845
2199
|
}
|
|
1846
2200
|
case 'pinepaper_register_custom_relation': {
|
|
1847
2201
|
const input = RegisterCustomRelationInputSchema.parse(args);
|
|
2202
|
+
CUSTOM_RELATION_NAMES.add(input.name);
|
|
1848
2203
|
const code = codeGenerator.generateRegisterCustomRelation(input);
|
|
1849
2204
|
return executeOrGenerate(code, `Registers custom relation type: ${input.name}`, options, 'pinepaper_register_custom_relation');
|
|
1850
2205
|
}
|
|
@@ -1986,10 +2341,9 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
1986
2341
|
case 'pinepaper_set_canvas_size': {
|
|
1987
2342
|
const input = SetCanvasSizeInputSchema.parse(args);
|
|
1988
2343
|
const code = codeGenerator.generateSetCanvasSize(input);
|
|
1989
|
-
const description =
|
|
1990
|
-
|
|
1991
|
-
height: input.height
|
|
1992
|
-
});
|
|
2344
|
+
const description = input.preset && input.width === undefined
|
|
2345
|
+
? `Set canvas size to preset ${input.preset}`
|
|
2346
|
+
: getLocalizedSuccessMessage(i18n, 'canvasSizeSet', { width: input.width ?? '', height: input.height ?? '' });
|
|
1993
2347
|
return executeOrGenerate(code, description, options, 'pinepaper_set_canvas_size');
|
|
1994
2348
|
}
|
|
1995
2349
|
case 'pinepaper_get_canvas_size': {
|
|
@@ -2007,8 +2361,13 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
2007
2361
|
return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
|
|
2008
2362
|
}
|
|
2009
2363
|
try {
|
|
2010
|
-
// Refresh the page
|
|
2011
|
-
await controller.refreshPage();
|
|
2364
|
+
// Refresh the page (recovers a dead session first; M6)
|
|
2365
|
+
const refreshed = await controller.refreshPage();
|
|
2366
|
+
const sizeNote = refreshed.relaunched
|
|
2367
|
+
? (refreshed.canvasSizeRestored
|
|
2368
|
+
? `\n\nThe browser session had been lost, so the browser was relaunched. The canvas size was restored to ${refreshed.canvasSizeRestored.width}x${refreshed.canvasSizeRestored.height}.`
|
|
2369
|
+
: '\n\nThe browser session had been lost, so the browser was relaunched at the default canvas size: set the size again if you need another.')
|
|
2370
|
+
: '';
|
|
2012
2371
|
// Wait a moment for the page to stabilize
|
|
2013
2372
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
2014
2373
|
// Take a screenshot to show the result
|
|
@@ -2020,7 +2379,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
|
|
|
2020
2379
|
|
|
2021
2380
|
The canvas is now completely empty. The welcome template will not appear (it only shows for first-time visitors).
|
|
2022
2381
|
|
|
2023
|
-
You can now start creating new items on a clean canvas
|
|
2382
|
+
You can now start creating new items on a clean canvas.${sizeNote}`,
|
|
2024
2383
|
},
|
|
2025
2384
|
];
|
|
2026
2385
|
if (screenshot) {
|
|
@@ -2090,6 +2449,15 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2090
2449
|
// -----------------------------------------------------------------------
|
|
2091
2450
|
case 'pinepaper_add_filter': {
|
|
2092
2451
|
const input = AddFilterInputSchema.parse(args);
|
|
2452
|
+
// Out-of-range parameters are refused with the engine's own range
|
|
2453
|
+
// (gate A, 1.6.19: dither levels 99 was accepted and quietly clamped).
|
|
2454
|
+
const ranges = FILTER_PARAM_RANGES[input.filterType] ?? {};
|
|
2455
|
+
for (const [k, v] of Object.entries((input.params ?? {}))) {
|
|
2456
|
+
const r = ranges[k];
|
|
2457
|
+
if (r && typeof v === 'number' && (v < r[0] || v > r[1])) {
|
|
2458
|
+
return errorResult(ErrorCodes.VALIDATION_ERROR, `${input.filterType} ${k} must be ${r[0]}..${r[1]} (got ${v}).`, { filterType: input.filterType, param: k, range: r });
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2093
2461
|
const code = codeGenerator.generateAddFilter(input.filterType, input.params);
|
|
2094
2462
|
const description = `Adds ${input.filterType} filter to the canvas`;
|
|
2095
2463
|
return executeOrGenerate(code, description, options, 'pinepaper_add_filter');
|
|
@@ -2300,7 +2668,19 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2300
2668
|
const perfTracker = getPerformanceTracker();
|
|
2301
2669
|
const recentMetrics = perfTracker.exportMetrics('json', { limit: metricsLimit });
|
|
2302
2670
|
const stats = perfTracker.getStats();
|
|
2303
|
-
|
|
2671
|
+
// exportMetrics('json') returns a JSON STRING; embedded as-is it reached
|
|
2672
|
+
// the report as one escaped string, not an array (gate H3).
|
|
2673
|
+
let entries = recentMetrics;
|
|
2674
|
+
if (typeof recentMetrics === 'string') {
|
|
2675
|
+
try {
|
|
2676
|
+
entries = JSON.parse(recentMetrics);
|
|
2677
|
+
}
|
|
2678
|
+
catch { /* keep the string */ }
|
|
2679
|
+
}
|
|
2680
|
+
if (entries && typeof entries === 'object' && !Array.isArray(entries) && Array.isArray(entries.metrics)) {
|
|
2681
|
+
entries = entries.metrics;
|
|
2682
|
+
}
|
|
2683
|
+
metricsSection = { entries, stats };
|
|
2304
2684
|
}
|
|
2305
2685
|
catch { /* metrics not available */ }
|
|
2306
2686
|
}
|
|
@@ -2355,17 +2735,25 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2355
2735
|
},
|
|
2356
2736
|
recommendations: [],
|
|
2357
2737
|
};
|
|
2358
|
-
//
|
|
2738
|
+
// RECOMMEND ONLY WHAT SAVES (gate H4, 1.6.19). This said "switch to
|
|
2739
|
+
// agent" on full, which saves under 1% (agent omits three small
|
|
2740
|
+
// tools). Each alternative is measured against the active set and
|
|
2741
|
+
// offered only when it saves at least 10%, with the saving stated.
|
|
2359
2742
|
const recs = toolDefsSection.recommendations;
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2743
|
+
const alternatives = [];
|
|
2744
|
+
for (const v of ['compact', 'minimal']) {
|
|
2745
|
+
for (const k of ['full', 'agent', 'minimal']) {
|
|
2746
|
+
if (k === toolkit && v === verbosity)
|
|
2747
|
+
continue;
|
|
2748
|
+
const t = Math.ceil(Buffer.byteLength(JSON.stringify(getToolsForToolkit(getToolsForVerbosity(v), k)), 'utf-8') / 4);
|
|
2749
|
+
alternatives.push({ label: `${k}+${v}`, how: `PINEPAPER_TOOLKIT=${k} PINEPAPER_VERBOSITY=${v}`, tokens: t });
|
|
2750
|
+
}
|
|
2368
2751
|
}
|
|
2752
|
+
alternatives
|
|
2753
|
+
.filter((alt) => alt.tokens <= activeTokens * 0.9)
|
|
2754
|
+
.sort((x, y) => x.tokens - y.tokens)
|
|
2755
|
+
.slice(0, 3)
|
|
2756
|
+
.forEach((alt) => recs.push(`${alt.how}: ~${alt.tokens.toLocaleString()} tokens/turn (${Math.round((1 - alt.tokens / activeTokens) * 100)}% less)${alt.label.startsWith('minimal+') ? ' — a small core set; switch profiles at runtime with pinepaper_set_toolkit' : ''}`));
|
|
2369
2757
|
if (activeTokens > 20000) {
|
|
2370
2758
|
recs.push(`Tool definitions alone consume ~${activeTokens.toLocaleString()} tokens per API turn — this is a major context budget item`);
|
|
2371
2759
|
}
|
|
@@ -2484,11 +2872,19 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2484
2872
|
// ASSET TOOLS
|
|
2485
2873
|
// -----------------------------------------------------------------------
|
|
2486
2874
|
case 'pinepaper_search_assets': {
|
|
2875
|
+
// Asked for by name, a removed repository says why rather than failing
|
|
2876
|
+
// the enum check with a bare "invalid option".
|
|
2877
|
+
{
|
|
2878
|
+
const { REMOVED_REPOSITORIES, REMOVED_REPOSITORY_REASON } = await import('../assets/index.js');
|
|
2879
|
+
if (typeof args.repository === 'string' && REMOVED_REPOSITORIES.includes(args.repository)) {
|
|
2880
|
+
return errorResult(ErrorCodes.INVALID_PARAMS, `${REMOVED_REPOSITORY_REASON} Search repository 'all', 'iconify' or 'fontawesome'.`, { repository: args.repository }, { toolName: 'pinepaper_search_assets' });
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2487
2883
|
const input = SearchAssetsInputSchema.parse(args);
|
|
2488
2884
|
const { getAssetManager } = await import('../assets/index.js');
|
|
2489
2885
|
const assetManager = getAssetManager();
|
|
2490
2886
|
try {
|
|
2491
|
-
const results = await assetManager.
|
|
2887
|
+
const { results, sources } = await assetManager.searchWithStatus(input.query, input.repository || 'all', input.limit || 10);
|
|
2492
2888
|
// If includeSvgContent is true, fetch SVG content for each result
|
|
2493
2889
|
let resultsWithContent = [];
|
|
2494
2890
|
if (input.includeSvgContent) {
|
|
@@ -2560,6 +2956,7 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2560
2956
|
query: input.query,
|
|
2561
2957
|
repository: input.repository || 'all',
|
|
2562
2958
|
count: resultsWithContent.length,
|
|
2959
|
+
sources,
|
|
2563
2960
|
includeSvgContent: input.includeSvgContent || false,
|
|
2564
2961
|
attributionRequired: requiresAttribution.length > 0,
|
|
2565
2962
|
attributionNotice,
|
|
@@ -2620,6 +3017,15 @@ You can now start creating new items on a clean canvas.`,
|
|
|
2620
3017
|
svg = body;
|
|
2621
3018
|
metadata = { source: 'url', url: input.url };
|
|
2622
3019
|
}
|
|
3020
|
+
// `color` was accepted and never applied, so a recoloured icon
|
|
3021
|
+
// rendered black (gate D15, 1.6.19). A monochrome icon draws in
|
|
3022
|
+
// currentColor (stroke sets) or in the default fill (fill sets):
|
|
3023
|
+
// both take the colour.
|
|
3024
|
+
if (input.color) {
|
|
3025
|
+
const c = input.color.replace(/"/g, '');
|
|
3026
|
+
svg = svg.replace(/currentColor/g, c);
|
|
3027
|
+
svg = svg.replace(/<svg\b([^>]*)>/i, (m, attrs) => (/\sfill\s*=/.test(attrs) ? m : `<svg${attrs} fill="${c}">`));
|
|
3028
|
+
}
|
|
2623
3029
|
// Import the SVG onto canvas using existing import_svg tool
|
|
2624
3030
|
const code = codeGenerator.generateImportSVG(svg, undefined, input.position, input.scale);
|
|
2625
3031
|
// A URL import has no catalogue metadata (no title, no licence): this
|
|
@@ -3150,10 +3556,18 @@ You can now start creating new items on a clean canvas.`,
|
|
|
3150
3556
|
`Call action 'list_styles' and use one marked composable.`, { style: input.style }, { toolName: 'pinepaper_design_system' });
|
|
3151
3557
|
}
|
|
3152
3558
|
const ops = designSystems.sceneToOps(scene);
|
|
3559
|
+
// Some vendored styles lay out at their OWN fixed size and ignore
|
|
3560
|
+
// width/height (swiss_typographic is always 1280x720), so a caller
|
|
3561
|
+
// asking for 1080x1080 got a resized canvas (gate run 3). Say so.
|
|
3562
|
+
const sizeIgnored = (input.width !== undefined && input.width !== scene.width)
|
|
3563
|
+
|| (input.height !== undefined && input.height !== scene.height);
|
|
3564
|
+
const sizeWarning = sizeIgnored
|
|
3565
|
+
? `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}; the requested ${input.width ?? '?'}x${input.height ?? '?'} was not used`
|
|
3566
|
+
: undefined;
|
|
3153
3567
|
if (input.draw === false) {
|
|
3154
|
-
return dataResult({ scene, ops, drawn: false });
|
|
3568
|
+
return dataResult({ scene, ops, drawn: false, ...(sizeWarning ? { warning: sizeWarning } : {}) });
|
|
3155
3569
|
}
|
|
3156
|
-
const code = codeGenerator.generateDesignCompose(scene, ops, scene.style);
|
|
3570
|
+
const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning);
|
|
3157
3571
|
return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
|
|
3158
3572
|
}
|
|
3159
3573
|
}
|
|
@@ -3171,7 +3585,12 @@ You can now start creating new items on a clean canvas.`,
|
|
|
3171
3585
|
case 'pinepaper_interchange': {
|
|
3172
3586
|
const input = InterchangeInputSchema.parse(args);
|
|
3173
3587
|
const code = codeGenerator.generateInterchange(input);
|
|
3174
|
-
|
|
3588
|
+
const out = await executeOrGenerate(code, `Interchange: ${input.action}`, options, 'pinepaper_interchange');
|
|
3589
|
+
// AN EXPORT IS A FILE (gate D7, 1.6.19): .lottie, GLB and the PNG-sequence
|
|
3590
|
+
// zip came back as byte-index objects or inline data URLs, and a Lottie
|
|
3591
|
+
// JSON can be large. Written out; the result names the path.
|
|
3592
|
+
const ext = { export_lottie: 'json', export_dotlottie: 'lottie', export_glb: 'glb', export_png_sequence: 'zip', export_bvh: 'bvh' };
|
|
3593
|
+
return ext[input.action] ? await writeResultDataToFile(out, ext[input.action]) : out;
|
|
3175
3594
|
}
|
|
3176
3595
|
case 'pinepaper_sound': {
|
|
3177
3596
|
const input = SoundInputSchema.parse(args);
|