@pascal-app/mcp 1.0.6 → 1.1.0
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.md +32 -11
- package/dist/bin/pascal-mcp.js +2 -0
- package/dist/bridge/agent-session.d.ts +2 -1
- package/dist/bridge/agent-session.d.ts.map +1 -1
- package/dist/bridge/agent-session.js +0 -1
- package/dist/bridge/scene-bridge.d.ts +22 -14
- package/dist/bridge/scene-bridge.d.ts.map +1 -1
- package/dist/bridge/scene-bridge.js +180 -198
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/safe-fetch.d.ts +6 -0
- package/dist/lib/safe-fetch.d.ts.map +1 -1
- package/dist/lib/safe-fetch.js +135 -41
- package/dist/operations/scene-operations.d.ts +7 -0
- package/dist/operations/scene-operations.d.ts.map +1 -1
- package/dist/operations/scene-operations.js +4 -0
- package/dist/prompts/from-brief.js +1 -1
- package/dist/prompts/iterate-on-feedback.js +1 -1
- package/dist/prompts/renovation-from-photos.js +1 -1
- package/dist/prompts/scene-guidance.js +2 -2
- package/dist/resources/agent-guide.d.ts.map +1 -1
- package/dist/resources/agent-guide.js +8 -3
- package/dist/resources/catalog-items.d.ts +4 -1
- package/dist/resources/catalog-items.d.ts.map +1 -1
- package/dist/resources/catalog-items.js +14 -6
- package/dist/server.d.ts +9 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +21 -5
- package/dist/templates/empty-studio.d.ts.map +1 -1
- package/dist/templates/empty-studio.js +2 -3
- package/dist/templates/garden-house.d.ts.map +1 -1
- package/dist/templates/garden-house.js +2 -3
- package/dist/templates/two-bedroom.d.ts.map +1 -1
- package/dist/templates/two-bedroom.js +2 -3
- package/dist/tools/add-column.d.ts.map +1 -1
- package/dist/tools/add-column.js +9 -6
- package/dist/tools/add-object.d.ts +44 -5
- package/dist/tools/add-object.d.ts.map +1 -1
- package/dist/tools/add-object.js +75 -21
- package/dist/tools/apply-patch.js +1 -1
- package/dist/tools/asset-catalog.d.ts +5 -6
- package/dist/tools/asset-catalog.d.ts.map +1 -1
- package/dist/tools/asset-catalog.js +1 -9
- package/dist/tools/author-scene.d.ts +8 -5
- package/dist/tools/author-scene.d.ts.map +1 -1
- package/dist/tools/author-scene.js +302 -51
- package/dist/tools/clear-scene.js +2 -2
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +8 -5
- package/dist/tools/honest-patch-guard.js +6 -5
- package/dist/tools/hosted-services.d.ts.map +1 -1
- package/dist/tools/hosted-services.js +2 -2
- package/dist/tools/index.d.ts +7 -2
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -9
- package/dist/tools/local-geometry-scripts.d.ts.map +1 -1
- package/dist/tools/local-geometry-scripts.js +7 -2
- package/dist/tools/patch-guards.d.ts.map +1 -1
- package/dist/tools/patch-guards.js +32 -15
- package/dist/tools/project-files.d.ts.map +1 -1
- package/dist/tools/project-files.js +2 -2
- package/dist/tools/room-tools.d.ts +1 -0
- package/dist/tools/room-tools.d.ts.map +1 -1
- package/dist/tools/room-tools.js +11 -8
- package/dist/tools/scene-scope.d.ts +4 -0
- package/dist/tools/scene-scope.d.ts.map +1 -0
- package/dist/tools/scene-scope.js +4 -0
- package/dist/tools/shared-tools.d.ts +0 -2
- package/dist/tools/shared-tools.d.ts.map +1 -1
- package/dist/tools/shared-tools.js +146 -31
- package/dist/tools/templates/create-house-from-brief.js +1 -1
- package/dist/tools/view-scene.js +2 -2
- package/examples/coordinate-conventions-demo.md +1 -0
- package/examples/photo-to-scene.md +1 -0
- package/examples/renovate-from-photos.md +2 -0
- package/package.json +10 -10
- package/dist/tools/create-unit.d.ts +0 -29
- package/dist/tools/create-unit.d.ts.map +0 -1
- package/dist/tools/create-unit.js +0 -36
- package/dist/tools/list-units.d.ts +0 -38
- package/dist/tools/list-units.d.ts.map +0 -1
- package/dist/tools/list-units.js +0 -61
- package/dist/tools/set-unit-members.d.ts +0 -22
- package/dist/tools/set-unit-members.d.ts.map +0 -1
- package/dist/tools/set-unit-members.js +0 -43
- package/dist/tools/unit-members.d.ts +0 -4
- package/dist/tools/unit-members.d.ts.map +0 -1
- package/dist/tools/unit-members.js +0 -24
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { filterDerivedNodeWrites } from '@pascal-app/core';
|
|
2
|
+
import { achievedChanges, authorSceneChecks, verifyScene, } from '@pascal-app/core/agent-operations';
|
|
3
|
+
import { authorScenePayload, authorSceneTool, isAgentRefusal, refuse, surfaceDescription, } from '@pascal-app/core/agent-tools';
|
|
3
4
|
import { GEOMETRY_SCRIPT_MIME_TYPES } from '@pascal-app/core/schema';
|
|
4
5
|
import { compileAuthorScene } from '@pascal-app/geometry-script/author-scene';
|
|
5
6
|
import { z } from 'zod';
|
|
@@ -8,15 +9,21 @@ import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations.js';
|
|
|
8
9
|
import { refusalResult, toolError } from './errors.js';
|
|
9
10
|
import { persistencePayload, publishLiveSceneSnapshot } from './live-sync.js';
|
|
10
11
|
import { toPatches } from './shared-tools.js';
|
|
12
|
+
/** The largest payload file read: a house with its modules inline runs to a few MiB. */
|
|
13
|
+
const PAYLOAD_FILE_MAX_BYTES = 32 * 1024 * 1024;
|
|
14
|
+
/** Payload keys holding a module's text; stored as `<key>Script`, the script's hash. */
|
|
15
|
+
const CODE_KEYS = new Set(['code', 'floorCode', 'wallCode']);
|
|
11
16
|
/**
|
|
12
|
-
* `author_scene` on the MCP:
|
|
13
|
-
* module and
|
|
14
|
-
*
|
|
17
|
+
* `author_scene` on the MCP: the payload given, or the one the adapter answers on the module (the
|
|
18
|
+
* stored module and adapter, changed by edits, when left out), or the stored conversion again;
|
|
19
|
+
* items naming a piece written with its module, those still being made left for the host to write
|
|
20
|
+
* when they land; every module compiled and stored through the host, the operation applied,
|
|
21
|
+
* Pascal's checks run, and the gate report answered.
|
|
15
22
|
*/
|
|
16
|
-
export function registerAuthorScene(server, bridge, host) {
|
|
23
|
+
export function registerAuthorScene(server, bridge, host, makers, readFile) {
|
|
17
24
|
server.registerTool(authorSceneTool.name, {
|
|
18
25
|
title: authorSceneTool.title,
|
|
19
|
-
description: authorSceneTool
|
|
26
|
+
description: surfaceDescription(authorSceneTool, 'mcp'),
|
|
20
27
|
inputSchema: z.object(authorSceneTool.input),
|
|
21
28
|
annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
22
29
|
}, async (input) => {
|
|
@@ -46,9 +53,39 @@ export function registerAuthorScene(server, bridge, host) {
|
|
|
46
53
|
? { materials: bridge.getMaterials() }
|
|
47
54
|
: {}),
|
|
48
55
|
};
|
|
56
|
+
const readText = async (sha256) => {
|
|
57
|
+
const bytes = await host.readArtifact({ sceneId: scene.id, sha256 });
|
|
58
|
+
if (!bytes)
|
|
59
|
+
throw new Error(`The stored script ${sha256} could not be read`);
|
|
60
|
+
return new TextDecoder().decode(bytes);
|
|
61
|
+
};
|
|
62
|
+
let source;
|
|
63
|
+
try {
|
|
64
|
+
const readPayloadFile = async (fileId) => {
|
|
65
|
+
if (!readFile)
|
|
66
|
+
refuse('files_unavailable', 'This Pascal server keeps no project files: pass the payload itself.');
|
|
67
|
+
return new TextDecoder().decode(await readFile({
|
|
68
|
+
projectId: scene.projectId ?? undefined,
|
|
69
|
+
fileId,
|
|
70
|
+
maxBytes: PAYLOAD_FILE_MAX_BYTES,
|
|
71
|
+
signal: host.signal,
|
|
72
|
+
}));
|
|
73
|
+
};
|
|
74
|
+
source = await sourceOf(args, storedConversion(before, args.buildingId), host, readText, readPayloadFile);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (isAgentRefusal(error))
|
|
78
|
+
return refusalResult(error);
|
|
79
|
+
return toolError(error instanceof Error ? error.message : String(error), {
|
|
80
|
+
code: 'conversion_failed',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if ('failed' in source)
|
|
84
|
+
return source.failed;
|
|
85
|
+
const pieces = await withPieces(source.payload, scene.id, makers);
|
|
49
86
|
let outcome;
|
|
50
87
|
try {
|
|
51
|
-
outcome = await compileAuthorScene(before, args, context, ({ code, kind, inputs, nodeId }) => compileAndStore(host, scene.id, { code, language: 'three', params: undefined, inputs }, kind, { nodeId }), host.signal);
|
|
88
|
+
outcome = await compileAuthorScene(before, { payload: pieces.payload, replace: args.replace, buildingId: args.buildingId }, context, ({ code, kind, inputs, nodeId }) => compileAndStore(host, scene.id, { code, language: 'three', params: undefined, inputs }, kind, { nodeId }), host.signal);
|
|
52
89
|
}
|
|
53
90
|
catch (error) {
|
|
54
91
|
if (isAgentRefusal(error))
|
|
@@ -59,54 +96,76 @@ export function registerAuthorScene(server, bridge, host) {
|
|
|
59
96
|
}
|
|
60
97
|
// Provenance: the design's module and the program that read it, kept as scripts of the scene.
|
|
61
98
|
const provenance = {};
|
|
62
|
-
for (const [
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
for (const kind of ['module', 'adapter']) {
|
|
100
|
+
const text = source.texts[kind];
|
|
101
|
+
const kept = source.changed[kind] && text !== undefined
|
|
102
|
+
? await host.storeArtifact({
|
|
103
|
+
sceneId: scene.id,
|
|
104
|
+
sha256: await sha256(text),
|
|
105
|
+
bytes: new TextEncoder().encode(text),
|
|
106
|
+
mimeType: GEOMETRY_SCRIPT_MIME_TYPES.three,
|
|
107
|
+
metadata: { kind: 'item' },
|
|
108
|
+
})
|
|
109
|
+
: source.stored[kind];
|
|
110
|
+
if (kept)
|
|
111
|
+
provenance[kind] = kept;
|
|
75
112
|
}
|
|
76
113
|
host.signal?.throwIfAborted();
|
|
77
114
|
await host.assertActive?.();
|
|
78
115
|
if (JSON.stringify(bridge.getNodes()) !== beforeJson) {
|
|
79
116
|
return toolError('The scene changed while its candidate was compiling. Read the current scene and retry the conversion.', { code: 'scene_changed', mutationApplied: false });
|
|
80
117
|
}
|
|
118
|
+
const report = {
|
|
119
|
+
...outcome.result,
|
|
120
|
+
defects: [...pieces.defects, ...outcome.result.defects],
|
|
121
|
+
};
|
|
122
|
+
const payloadText = storedPayloadText(await withScripts(source.payload));
|
|
123
|
+
const payloadScript = await host.storeArtifact({
|
|
124
|
+
sceneId: scene.id,
|
|
125
|
+
sha256: await sha256(payloadText),
|
|
126
|
+
bytes: new TextEncoder().encode(payloadText),
|
|
127
|
+
mimeType: GEOMETRY_SCRIPT_MIME_TYPES.three,
|
|
128
|
+
metadata: { kind: 'item' },
|
|
129
|
+
});
|
|
81
130
|
const buildingUpdate = outcome.changes.update.find((change) => change.id === outcome.buildingId);
|
|
82
|
-
|
|
83
|
-
...
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
report: outcome.result,
|
|
88
|
-
at: new Date().toISOString(),
|
|
89
|
-
},
|
|
131
|
+
const stored = {
|
|
132
|
+
...provenance,
|
|
133
|
+
payload: payloadScript,
|
|
134
|
+
pending: pieces.pending,
|
|
135
|
+
at: new Date().toISOString(),
|
|
90
136
|
};
|
|
91
|
-
|
|
137
|
+
buildingUpdate.data.metadata = JSON.parse(JSON.stringify({ ...buildingUpdate.data.metadata, authorScene: stored }));
|
|
138
|
+
// A revision's plates and ceilings follow its walls: what only the reconciler may change on
|
|
139
|
+
// one that stays (its outline, its room) is left to the bridge's reconciliation after the
|
|
140
|
+
// write, as in the editor; a new room's arrive with it.
|
|
141
|
+
const patches = toPatches(filterDerivedNodeWrites(bridge.getNodes(), outcome.changes));
|
|
92
142
|
if (patches.length)
|
|
93
143
|
bridge.applyPatch(patches);
|
|
94
|
-
const
|
|
144
|
+
const verified = verifyScene(bridge.getNodes(), undefined, {
|
|
95
145
|
activeLevelId: null,
|
|
96
146
|
}).result;
|
|
97
147
|
const byType = new Map();
|
|
98
|
-
for (const issue of
|
|
148
|
+
for (const issue of verified.issues)
|
|
99
149
|
byType.set(issue.type ?? 'unknown', (byType.get(issue.type ?? 'unknown') ?? 0) + 1);
|
|
150
|
+
const checks = authorSceneChecks({
|
|
151
|
+
payload: source.payload,
|
|
152
|
+
report,
|
|
153
|
+
pending: pieces.pending,
|
|
154
|
+
issues: verified.issues.filter((issue) => issue.severity !== 'info'),
|
|
155
|
+
});
|
|
100
156
|
const payload = {
|
|
101
|
-
...
|
|
157
|
+
...report,
|
|
158
|
+
pending: pieces.pending,
|
|
159
|
+
checks,
|
|
160
|
+
complete: pieces.pending.length === 0 && checks.every((check) => check.passed),
|
|
102
161
|
itemTriangles: outcome.itemTriangles,
|
|
103
162
|
itemsCompiled: outcome.itemsCompiled,
|
|
104
|
-
green:
|
|
163
|
+
green: report.defects.length === 0,
|
|
105
164
|
verify: {
|
|
106
|
-
valid:
|
|
107
|
-
issues:
|
|
165
|
+
valid: verified.valid,
|
|
166
|
+
issues: verified.issues.length,
|
|
108
167
|
byType: Object.fromEntries(byType),
|
|
109
|
-
first:
|
|
168
|
+
first: verified.issues.slice(0, 40).map((i) => i.message),
|
|
110
169
|
},
|
|
111
170
|
achieved: achievedChanges(before, outcome.changes ?? {}),
|
|
112
171
|
...persistencePayload(await publishLiveSceneSnapshot(bridge, authorSceneTool.name)),
|
|
@@ -117,23 +176,215 @@ export function registerAuthorScene(server, bridge, host) {
|
|
|
117
176
|
};
|
|
118
177
|
});
|
|
119
178
|
}
|
|
120
|
-
/** The
|
|
121
|
-
function
|
|
179
|
+
/** The building a conversion revises: the one named, else the one holding a conversion. */
|
|
180
|
+
function storedConversion(nodes, buildingId) {
|
|
181
|
+
const holds = (node) => node?.type === 'building'
|
|
182
|
+
? (node.metadata?.authorScene ?? null)
|
|
183
|
+
: null;
|
|
184
|
+
if (buildingId)
|
|
185
|
+
return holds(nodes[buildingId]) ?? undefined;
|
|
186
|
+
return Object.values(nodes).map(holds).find(Boolean) ?? undefined;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The payload to write, with the module and adapter it came from: given, or read from its project
|
|
190
|
+
* file; answered by the adapter on the module (each the one given or stored, changed by the edits);
|
|
191
|
+
* or the stored conversion again.
|
|
192
|
+
* An adapter that throws, or answers no valid payload, is answered with why, and nothing is written.
|
|
193
|
+
*/
|
|
194
|
+
async function sourceOf(args, stored, host, readText, readPayloadFile) {
|
|
195
|
+
const edits = args.edits ?? [];
|
|
196
|
+
const changed = {
|
|
197
|
+
module: args.module !== undefined || edits.some((edit) => edit.in === 'module'),
|
|
198
|
+
adapter: args.adapter !== undefined || edits.some((edit) => edit.in === 'adapter'),
|
|
199
|
+
};
|
|
200
|
+
const result = (payload, texts) => ({
|
|
201
|
+
payload,
|
|
202
|
+
texts,
|
|
203
|
+
changed,
|
|
204
|
+
stored: { module: stored?.module, adapter: stored?.adapter },
|
|
205
|
+
});
|
|
206
|
+
if (args.payload && args.payloadFile)
|
|
207
|
+
refuse('payload_twice', 'Pass the payload or its payloadFile, not both.');
|
|
208
|
+
if (args.payload || args.payloadFile) {
|
|
209
|
+
if (edits.length)
|
|
210
|
+
refuse('edits_with_payload', 'Edits change the module or the adapter Pascal runs: leave the payload out to have it run.');
|
|
211
|
+
const texts = { module: args.module, adapter: args.adapter };
|
|
212
|
+
if (args.payload)
|
|
213
|
+
return result(args.payload, texts);
|
|
214
|
+
let answered;
|
|
215
|
+
try {
|
|
216
|
+
answered = JSON.parse(await readPayloadFile(args.payloadFile));
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (!(error instanceof SyntaxError))
|
|
220
|
+
throw error;
|
|
221
|
+
return {
|
|
222
|
+
failed: toolError(`The payload file is not JSON: ${error.message}`, {
|
|
223
|
+
code: 'payload_invalid',
|
|
224
|
+
mutationApplied: false,
|
|
225
|
+
}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const parsed = parsedPayload(answered, 'The payload file');
|
|
229
|
+
return 'failed' in parsed ? parsed : result(parsed.payload, texts);
|
|
230
|
+
}
|
|
231
|
+
if (args.module !== undefined || args.adapter !== undefined || edits.length) {
|
|
232
|
+
const texts = {
|
|
233
|
+
module: args.module ?? (stored?.module ? await readText(stored.module) : undefined),
|
|
234
|
+
adapter: args.adapter ?? (stored?.adapter ? await readText(stored.adapter) : undefined),
|
|
235
|
+
};
|
|
236
|
+
edits.forEach((edit, index) => {
|
|
237
|
+
const text = texts[edit.in];
|
|
238
|
+
if (text === undefined)
|
|
239
|
+
refuse('edit_without_source', `Edit ${index}: there is no ${edit.in} to change. Pass it, or convert with one first.`, { edit: index });
|
|
240
|
+
const at = text.indexOf(edit.find);
|
|
241
|
+
if (at < 0)
|
|
242
|
+
refuse('edit_not_found', `Edit ${index}: its find text is not in the ${edit.in}.`, {
|
|
243
|
+
edit: index,
|
|
244
|
+
});
|
|
245
|
+
if (text.indexOf(edit.find, at + 1) >= 0)
|
|
246
|
+
refuse('edit_ambiguous', `Edit ${index}: its find text occurs more than once in the ${edit.in}; give more of it.`, { edit: index });
|
|
247
|
+
texts[edit.in] = text.slice(0, at) + edit.replace + text.slice(at + edit.find.length);
|
|
248
|
+
});
|
|
249
|
+
if (texts.adapter === undefined)
|
|
250
|
+
refuse('adapter_required', 'This scene stores no adapter: pass the payload, or the module and its adapter together in one call.');
|
|
251
|
+
if (texts.module === undefined)
|
|
252
|
+
refuse('module_required', 'This scene stores no module: pass the module and its adapter together in one call.');
|
|
253
|
+
if (!host.runAdapter)
|
|
254
|
+
refuse('adapter_unavailable', 'This Pascal server cannot run an adapter: pass the payload.');
|
|
255
|
+
let answered;
|
|
256
|
+
try {
|
|
257
|
+
answered = (await host.runAdapter({ adapter: texts.adapter, design: texts.module })).payload;
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return {
|
|
261
|
+
failed: toolError(`Your adapter failed: ${error instanceof Error ? error.message : String(error)}`, { code: 'adapter_failed', mutationApplied: false }),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const parsed = parsedPayload(answered, "Your adapter's payload");
|
|
265
|
+
return 'failed' in parsed ? parsed : result(parsed.payload, texts);
|
|
266
|
+
}
|
|
267
|
+
if (typeof stored?.payload !== 'string')
|
|
268
|
+
refuse('nothing_to_write', 'Pass a payload, or a module and its adapter: this scene holds no stored conversion to write again.');
|
|
269
|
+
const rehydrated = (await withCode(readStoredPayload(await readText(stored.payload)), readText));
|
|
270
|
+
const parsed = authorScenePayload.safeParse(rehydrated);
|
|
271
|
+
if (!parsed.success)
|
|
272
|
+
refuse('stored_conversion_unreadable', 'The stored conversion could not be read back: pass the payload, or the module and its adapter.');
|
|
273
|
+
return result(parsed.data, {});
|
|
274
|
+
}
|
|
275
|
+
/** A payload read from an adapter or a file, or why it does not match author_scene's. */
|
|
276
|
+
function parsedPayload(value, what) {
|
|
277
|
+
const parsed = authorScenePayload.safeParse(value);
|
|
278
|
+
if (parsed.success)
|
|
279
|
+
return { payload: parsed.data };
|
|
122
280
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
rooms: level.rooms.map(({ floorCode: _, wallCode: _w, ...room }) => room),
|
|
134
|
-
})),
|
|
281
|
+
failed: toolError(`${what} does not match author_scene's: ${parsed.error.issues
|
|
282
|
+
.slice(0, 12)
|
|
283
|
+
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
|
|
284
|
+
.join('; ')}`, {
|
|
285
|
+
code: 'payload_invalid',
|
|
286
|
+
mutationApplied: false,
|
|
287
|
+
issues: parsed.error.issues
|
|
288
|
+
.slice(0, 40)
|
|
289
|
+
.map((issue) => ({ path: issue.path.join('.'), message: issue.message })),
|
|
290
|
+
}),
|
|
135
291
|
};
|
|
136
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Items naming a piece, with its module once made; those still being made left out and listed as
|
|
295
|
+
* pending (the host writes them when they land); one that failed, is unknown or has no module, a
|
|
296
|
+
* defect by its id.
|
|
297
|
+
*/
|
|
298
|
+
async function withPieces(payload, sceneId, makers) {
|
|
299
|
+
const items = [
|
|
300
|
+
...payload.levels.flatMap((level) => level.items ?? []),
|
|
301
|
+
...(payload.roof?.items ?? []),
|
|
302
|
+
];
|
|
303
|
+
const ids = [...new Set(items.flatMap((item) => (item.piece ? [item.piece] : [])))];
|
|
304
|
+
const states = ids.length && makers ? await makers.pieces({ sceneId, ids }) : {};
|
|
305
|
+
const pending = [];
|
|
306
|
+
const defects = [];
|
|
307
|
+
const resolve = (list) => list?.flatMap(({ piece, ...item }) => {
|
|
308
|
+
if (!piece) {
|
|
309
|
+
if (item.code)
|
|
310
|
+
return [item];
|
|
311
|
+
defects.push({ id: item.id, code: 'no_module', message: 'It has neither code nor piece.' });
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
const state = states[piece];
|
|
315
|
+
if (state?.status === 'made')
|
|
316
|
+
return [{ ...item, code: state.code }];
|
|
317
|
+
if (state?.status === 'making')
|
|
318
|
+
pending.push({ id: item.id, piece });
|
|
319
|
+
else
|
|
320
|
+
defects.push(!makers
|
|
321
|
+
? {
|
|
322
|
+
id: item.id,
|
|
323
|
+
code: 'pieces_unavailable',
|
|
324
|
+
message: 'This Pascal server makes no pieces: give the item its code.',
|
|
325
|
+
}
|
|
326
|
+
: state
|
|
327
|
+
? { id: item.id, code: 'piece_failed', message: state.error }
|
|
328
|
+
: {
|
|
329
|
+
id: item.id,
|
|
330
|
+
code: 'piece_unknown',
|
|
331
|
+
message: `No piece "${piece}": make it with add_object's piece first.`,
|
|
332
|
+
});
|
|
333
|
+
return [];
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
payload: {
|
|
337
|
+
...payload,
|
|
338
|
+
levels: payload.levels.map((level) => ({ ...level, items: resolve(level.items) })),
|
|
339
|
+
...(payload.roof ? { roof: { ...payload.roof, items: resolve(payload.roof.items) } } : {}),
|
|
340
|
+
},
|
|
341
|
+
pending,
|
|
342
|
+
defects,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const STORED_PAYLOAD_PREFIX = 'export default ';
|
|
346
|
+
/** The payload as a module of the scene, the one script type its files take. */
|
|
347
|
+
const storedPayloadText = (payload) => `${STORED_PAYLOAD_PREFIX}${JSON.stringify(payload)}\n`;
|
|
348
|
+
function readStoredPayload(text) {
|
|
349
|
+
if (!text.startsWith(STORED_PAYLOAD_PREFIX))
|
|
350
|
+
return undefined;
|
|
351
|
+
try {
|
|
352
|
+
return JSON.parse(text.slice(STORED_PAYLOAD_PREFIX.length));
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
return undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** The payload as kept with the scene: every module as its stored script's hash, pieces by id. */
|
|
359
|
+
async function withScripts(value) {
|
|
360
|
+
if (Array.isArray(value))
|
|
361
|
+
return Promise.all(value.map(withScripts));
|
|
362
|
+
if (!value || typeof value !== 'object')
|
|
363
|
+
return value;
|
|
364
|
+
const out = {};
|
|
365
|
+
for (const [key, entry] of Object.entries(value))
|
|
366
|
+
if (CODE_KEYS.has(key) && typeof entry === 'string')
|
|
367
|
+
out[`${key}Script`] = await sha256(entry);
|
|
368
|
+
else
|
|
369
|
+
out[key] = await withScripts(entry);
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
/** The stored payload with its modules read back; one whose script is gone is left without it. */
|
|
373
|
+
async function withCode(value, readText) {
|
|
374
|
+
if (Array.isArray(value))
|
|
375
|
+
return Promise.all(value.map((entry) => withCode(entry, readText)));
|
|
376
|
+
if (!value || typeof value !== 'object')
|
|
377
|
+
return value;
|
|
378
|
+
const out = {};
|
|
379
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
380
|
+
const base = key.endsWith('Script') ? key.slice(0, -'Script'.length) : null;
|
|
381
|
+
if (base && CODE_KEYS.has(base) && typeof entry === 'string')
|
|
382
|
+
out[base] = await readText(entry).catch(() => undefined);
|
|
383
|
+
else
|
|
384
|
+
out[key] = await withCode(entry, readText);
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
137
388
|
async function sha256(text) {
|
|
138
389
|
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
139
390
|
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearSceneTool } from '@pascal-app/core/agent-tools';
|
|
1
|
+
import { clearSceneTool, surfaceDescription } from '@pascal-app/core/agent-tools';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { DESTRUCTIVE_TOOL_ANNOTATIONS } from './annotations.js';
|
|
4
4
|
import { refusalResult } from './errors.js';
|
|
@@ -11,7 +11,7 @@ import { liveSyncOutput, persistencePayload, publishLiveSceneSnapshot } from './
|
|
|
11
11
|
export function registerClearScene(server, operations) {
|
|
12
12
|
server.registerTool(clearSceneTool.name, {
|
|
13
13
|
title: clearSceneTool.title,
|
|
14
|
-
description: clearSceneTool
|
|
14
|
+
description: surfaceDescription(clearSceneTool, 'mcp'),
|
|
15
15
|
inputSchema: z.object(clearSceneTool.input),
|
|
16
16
|
outputSchema: z.object({
|
|
17
17
|
cleared: z.object({ removed: z.number() }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/tools/collections.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/tools/collections.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAiB7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAiBpD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAgDxF"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { editCollection, listCollections, writeCollections, } from '@pascal-app/core/agent-operations';
|
|
2
|
-
import { editCollectionTool, listCollectionsTool } from '@pascal-app/core/agent-tools';
|
|
2
|
+
import { editCollectionTool, listCollectionsTool, surfaceDescription, } from '@pascal-app/core/agent-tools';
|
|
3
|
+
import { untracked } from '@pascal-app/core/runtime';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import { DESTRUCTIVE_TOOL_ANNOTATIONS, READ_ONLY_TOOL_ANNOTATIONS } from './annotations.js';
|
|
5
6
|
import { refusalResult } from './errors.js';
|
|
6
7
|
import { persistencePayload, publishLiveSceneSnapshot } from './live-sync.js';
|
|
8
|
+
import { inScene } from './scene-scope.js';
|
|
7
9
|
// The collection tools share contracts and operations with the chat; they read the scene's
|
|
8
10
|
// collections beside its nodes, which the node-only shared operations never see.
|
|
9
11
|
function sceneOf(bridge) {
|
|
@@ -15,7 +17,7 @@ function reply(payload) {
|
|
|
15
17
|
export function registerCollectionTools(server, bridge) {
|
|
16
18
|
server.registerTool(listCollectionsTool.name, {
|
|
17
19
|
title: listCollectionsTool.title,
|
|
18
|
-
description: listCollectionsTool
|
|
20
|
+
description: surfaceDescription(listCollectionsTool, 'mcp'),
|
|
19
21
|
inputSchema: z.object(listCollectionsTool.input),
|
|
20
22
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
21
23
|
}, async (input) => {
|
|
@@ -28,13 +30,13 @@ export function registerCollectionTools(server, bridge) {
|
|
|
28
30
|
});
|
|
29
31
|
server.registerTool(editCollectionTool.name, {
|
|
30
32
|
title: editCollectionTool.title,
|
|
31
|
-
description: editCollectionTool
|
|
33
|
+
description: surfaceDescription(editCollectionTool, 'mcp'),
|
|
32
34
|
inputSchema: z.object(editCollectionTool.input),
|
|
33
35
|
annotations: DESTRUCTIVE_TOOL_ANNOTATIONS,
|
|
34
36
|
}, async (input) => {
|
|
35
37
|
let outcome;
|
|
36
38
|
try {
|
|
37
|
-
outcome = editCollection(sceneOf(bridge), input);
|
|
39
|
+
outcome = inScene(bridge, () => editCollection(sceneOf(bridge), input));
|
|
38
40
|
}
|
|
39
41
|
catch (error) {
|
|
40
42
|
return refusalResult(error);
|
|
@@ -43,7 +45,8 @@ export function registerCollectionTools(server, bridge) {
|
|
|
43
45
|
bridge.runAsSingleHistoryStep(() => {
|
|
44
46
|
if (update?.length)
|
|
45
47
|
bridge.applyPatch(update.map(({ id, data }) => ({ op: 'update', id: id, data })));
|
|
46
|
-
|
|
48
|
+
// Writing the other collections back is a copy, not a read of them.
|
|
49
|
+
bridge.setCollections(writeCollections(untracked(bridge.getCollections()), collections));
|
|
47
50
|
});
|
|
48
51
|
return reply({
|
|
49
52
|
...outcome.result,
|
|
@@ -7,14 +7,15 @@ import { PatchRefusedError, registerPatchGuard } from './patch-guards.js';
|
|
|
7
7
|
* one hides is refused, naming it; `null` clears. The cleaned data is what is written.
|
|
8
8
|
*/
|
|
9
9
|
function honestUpdates(patches, scene) {
|
|
10
|
-
|
|
10
|
+
// The batch over the scene, read id by id.
|
|
11
|
+
const written = new Map();
|
|
11
12
|
for (const [index, patch] of patches.entries()) {
|
|
12
13
|
if (patch.op === 'create')
|
|
13
|
-
|
|
14
|
+
written.set(patch.node.id, patch.node);
|
|
14
15
|
else if (patch.op === 'delete')
|
|
15
|
-
|
|
16
|
+
written.set(patch.id, undefined);
|
|
16
17
|
else if (patch.op === 'update') {
|
|
17
|
-
const current =
|
|
18
|
+
const current = written.has(patch.id) ? written.get(patch.id) : scene[patch.id];
|
|
18
19
|
if (!current)
|
|
19
20
|
continue;
|
|
20
21
|
try {
|
|
@@ -25,7 +26,7 @@ function honestUpdates(patches, scene) {
|
|
|
25
26
|
throw error;
|
|
26
27
|
throw new PatchRefusedError(error.code, index, patch.id, error.message);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
written.set(patch.id, { ...current, ...patch.data });
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hosted-services.d.ts","sourceRoot":"","sources":["../../src/tools/hosted-services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,mCAAmC,CAAA;
|
|
1
|
+
{"version":3,"file":"hosted-services.d.ts","sourceRoot":"","sources":["../../src/tools/hosted-services.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,mCAAmC,CAAA;AAS1C,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,qBAAqB,GAC7B,IAAI,CA8CN"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { executeHostedServiceTool, } from '@pascal-app/core/agent-operations';
|
|
2
|
-
import { HOSTED_SERVICE_TOOL_CONTRACTS, isAgentRefusal, refusalPayload, } from '@pascal-app/core/agent-tools';
|
|
2
|
+
import { HOSTED_SERVICE_TOOL_CONTRACTS, isAgentRefusal, refusalPayload, surfaceDescription, } from '@pascal-app/core/agent-tools';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
export function registerHostedServiceTools(server, execute) {
|
|
5
5
|
for (const contract of HOSTED_SERVICE_TOOL_CONTRACTS) {
|
|
6
6
|
server.registerTool(contract.name, {
|
|
7
7
|
title: contract.title,
|
|
8
|
-
description: contract
|
|
8
|
+
description: surfaceDescription(contract, 'mcp'),
|
|
9
9
|
inputSchema: z.object(contract.input).strict(),
|
|
10
10
|
annotations: contract.annotations,
|
|
11
11
|
}, async (input, ctx) => {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import type { ProjectFileReader } from '@pascal-app/core/agent-operations';
|
|
2
3
|
import type { SceneOperations } from '../operations/index.js';
|
|
3
|
-
import { type GeometryScriptHost } from './add-object.js';
|
|
4
|
+
import { type GeometryScriptHost, type ObjectMakerHost } from './add-object.js';
|
|
4
5
|
import type { AssetCatalog } from './asset-catalog.js';
|
|
5
6
|
import { type PlanReadingHosts } from './shared-tools.js';
|
|
6
7
|
import { type SceneViewHost } from './view-scene.js';
|
|
@@ -16,7 +17,11 @@ import { type SceneViewHost } from './view-scene.js';
|
|
|
16
17
|
export type ToolHosts = PlanReadingHosts & {
|
|
17
18
|
catalog?: AssetCatalog;
|
|
18
19
|
geometryScripts?: GeometryScriptHost;
|
|
20
|
+
/** Makes add_object's pieces from words; without it a piece is refused `pieces_unavailable`. */
|
|
21
|
+
objectMakers?: ObjectMakerHost;
|
|
22
|
+
/** Reads author_scene's payloadFile; without it the tool refuses one `files_unavailable`. */
|
|
23
|
+
readFile?: ProjectFileReader;
|
|
19
24
|
sceneViews?: SceneViewHost;
|
|
20
25
|
};
|
|
21
|
-
export declare function registerTools(server: McpServer, operations: SceneOperations, { catalog, geometryScripts, sceneViews, assetViews }?: ToolHosts): void;
|
|
26
|
+
export declare function registerTools(server: McpServer, operations: SceneOperations, { catalog, geometryScripts, objectMakers, readFile, sceneViews, assetViews }?: ToolHosts): void;
|
|
22
27
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,OAAO,EAEL,KAAK,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAA;AAC1E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAEpD,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EAGrB,MAAM,cAAc,CAAA;AAErB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAcnD,OAAO,EAAE,KAAK,gBAAgB,EAAuB,MAAM,gBAAgB,CAAA;AAI3E,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAA;AAEpE;;;;;;;GAOG;AACH,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG;IACzC,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,eAAe,CAAC,EAAE,kBAAkB,CAAA;IACpC,gGAAgG;IAChG,YAAY,CAAC,EAAE,eAAe,CAAA;IAC9B,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,iBAAiB,CAAA;IAC5B,UAAU,CAAC,EAAE,aAAa,CAAA;CAC3B,CAAA;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,SAAS,EACjB,UAAU,EAAE,eAAe,EAC3B,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,GAAE,SAAc,GAC3F,IAAI,CA4BN"}
|
package/dist/tools/index.js
CHANGED
|
@@ -5,32 +5,29 @@ import { registerAuthorScene } from './author-scene.js';
|
|
|
5
5
|
import { registerCheckCollisions } from './check-collisions.js';
|
|
6
6
|
import { registerClearScene } from './clear-scene.js';
|
|
7
7
|
import { registerConstructionTools } from './construction-tools.js';
|
|
8
|
-
import { registerCreateUnit } from './create-unit.js';
|
|
9
8
|
import { registerExportGlb } from './export-glb.js';
|
|
10
9
|
import { registerExportJson } from './export-json.js';
|
|
11
10
|
import { registerGetScene } from './get-scene.js';
|
|
12
|
-
import { registerListUnits } from './list-units.js';
|
|
13
11
|
import { registerMeasure } from './measure.js';
|
|
14
12
|
import { registerPhotoToSceneTool } from './photo-to-scene/index.js';
|
|
15
13
|
import { registerRedo } from './redo.js';
|
|
16
14
|
import { registerRoomTools } from './room-tools.js';
|
|
17
15
|
import { registerSceneLifecycleTools } from './scene-lifecycle/index.js';
|
|
18
|
-
import { registerSetUnitMembers } from './set-unit-members.js';
|
|
19
16
|
import { registerSetZone } from './set-zone.js';
|
|
20
17
|
import { registerSharedTools } from './shared-tools.js';
|
|
21
18
|
import { registerTemplateTools } from './templates/index.js';
|
|
22
19
|
import { registerUndo } from './undo.js';
|
|
23
20
|
import { registerVariantTools } from './variants/index.js';
|
|
24
21
|
import { registerViewScene } from './view-scene.js';
|
|
25
|
-
export function registerTools(server, operations, { catalog, geometryScripts, sceneViews, assetViews } = {}) {
|
|
22
|
+
export function registerTools(server, operations, { catalog, geometryScripts, objectMakers, readFile, sceneViews, assetViews } = {}) {
|
|
26
23
|
registerGetScene(server, operations);
|
|
27
24
|
registerSharedTools(server, operations, catalog, {
|
|
28
25
|
assetViews,
|
|
29
26
|
buildObject: (input) => compileObjectOnHost(geometryScripts, operations, input),
|
|
30
27
|
});
|
|
31
28
|
registerAddColumn(server, operations, geometryScripts);
|
|
32
|
-
registerAddObject(server, operations, geometryScripts);
|
|
33
|
-
registerAuthorScene(server, operations, geometryScripts);
|
|
29
|
+
registerAddObject(server, operations, geometryScripts, objectMakers);
|
|
30
|
+
registerAuthorScene(server, operations, geometryScripts, objectMakers, readFile);
|
|
34
31
|
registerGetSource(server, operations, geometryScripts);
|
|
35
32
|
registerMeasure(server, operations);
|
|
36
33
|
registerViewScene(server, operations, sceneViews);
|
|
@@ -38,9 +35,6 @@ export function registerTools(server, operations, { catalog, geometryScripts, sc
|
|
|
38
35
|
registerRoomTools(server, operations, geometryScripts);
|
|
39
36
|
registerApplyPatch(server, operations);
|
|
40
37
|
registerClearScene(server, operations);
|
|
41
|
-
registerCreateUnit(server, operations);
|
|
42
|
-
registerSetUnitMembers(server, operations);
|
|
43
|
-
registerListUnits(server, operations);
|
|
44
38
|
registerSetZone(server, operations);
|
|
45
39
|
registerUndo(server, operations);
|
|
46
40
|
registerRedo(server, operations);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-geometry-scripts.d.ts","sourceRoot":"","sources":["../../src/tools/local-geometry-scripts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"local-geometry-scripts.d.ts","sourceRoot":"","sources":["../../src/tools/local-geometry-scripts.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEtD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CAkE5E"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { sdfChecksOn } from '@pascal-app/core/usage-field';
|
|
3
4
|
import { compileBlenderBuild } from '@pascal-app/geometry-script/blender';
|
|
4
5
|
import { runBlenderLocally } from '@pascal-app/geometry-script/blender-local';
|
|
5
6
|
import { DiskArtifactStore } from '../storage/disk-artifact-store.js';
|
|
@@ -17,7 +18,11 @@ export function localGeometryScripts(projectPath) {
|
|
|
17
18
|
if (!blender)
|
|
18
19
|
throw new Error('bpy scripts need PASCAL_BLENDER, the path to a Blender binary');
|
|
19
20
|
const run = await runBlenderLocally({ blender, code: input.code, params: input.params });
|
|
20
|
-
const { glb, ...compiled } = await compileBlenderBuild({
|
|
21
|
+
const { glb, ...compiled } = await compileBlenderBuild({
|
|
22
|
+
...input,
|
|
23
|
+
run,
|
|
24
|
+
...(sdfChecksOn() ? { usageShape: true } : {}),
|
|
25
|
+
});
|
|
21
26
|
return { ...compiled, glb: new Uint8Array(glb) };
|
|
22
27
|
}
|
|
23
28
|
if (process.env.PASCAL_SERVER_SCRIPT_COMPILE !== '1') {
|
|
@@ -55,7 +60,7 @@ export function localGeometryScripts(projectPath) {
|
|
|
55
60
|
reject(new Error(`Geometry script compilation failed: ${stderr}`));
|
|
56
61
|
}
|
|
57
62
|
});
|
|
58
|
-
child.stdin.end(JSON.stringify(input));
|
|
63
|
+
child.stdin.end(JSON.stringify({ ...input, ...(sdfChecksOn() ? { usageShape: true } : {}) }));
|
|
59
64
|
});
|
|
60
65
|
},
|
|
61
66
|
storeArtifact: async ({ sha256, bytes }) => {
|