@pinepaper.studio/mcp-server 1.6.26 → 1.6.27

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.
Files changed (67) hide show
  1. package/README.md +71 -1
  2. package/SECURITY.md +2 -2
  3. package/dist/agent/types.d.ts +3 -3
  4. package/dist/browser/puppeteer-controller.d.ts +11 -0
  5. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  6. package/dist/browser/puppeteer-controller.js +38 -2
  7. package/dist/browser/puppeteer-controller.js.map +1 -1
  8. package/dist/build-info.json +2 -2
  9. package/dist/ontology/hints.d.ts +3 -1
  10. package/dist/ontology/hints.d.ts.map +1 -1
  11. package/dist/ontology/hints.js +4 -2
  12. package/dist/ontology/hints.js.map +1 -1
  13. package/dist/ontology/upstream-vocabulary.d.ts +1 -1
  14. package/dist/ontology/upstream-vocabulary.js +1 -1
  15. package/dist/ontology/vocabulary.d.ts.map +1 -1
  16. package/dist/ontology/vocabulary.js +2 -0
  17. package/dist/ontology/vocabulary.js.map +1 -1
  18. package/dist/tools/definitions.d.ts.map +1 -1
  19. package/dist/tools/definitions.js +23 -10
  20. package/dist/tools/definitions.js.map +1 -1
  21. package/dist/tools/engine-lists.d.ts +11 -1
  22. package/dist/tools/engine-lists.d.ts.map +1 -1
  23. package/dist/tools/engine-lists.js +109 -1
  24. package/dist/tools/engine-lists.js.map +1 -1
  25. package/dist/tools/engine-methods.d.ts.map +1 -1
  26. package/dist/tools/engine-methods.js +1 -0
  27. package/dist/tools/engine-methods.js.map +1 -1
  28. package/dist/tools/engine-surface.d.ts +1 -1
  29. package/dist/tools/engine-surface.d.ts.map +1 -1
  30. package/dist/tools/engine-surface.js +7 -4
  31. package/dist/tools/engine-surface.js.map +1 -1
  32. package/dist/tools/export-file-receipt.d.ts +43 -0
  33. package/dist/tools/export-file-receipt.d.ts.map +1 -1
  34. package/dist/tools/export-file-receipt.js +61 -6
  35. package/dist/tools/export-file-receipt.js.map +1 -1
  36. package/dist/tools/expression-grammar.d.ts +10 -0
  37. package/dist/tools/expression-grammar.d.ts.map +1 -0
  38. package/dist/tools/expression-grammar.js +33 -0
  39. package/dist/tools/expression-grammar.js.map +1 -0
  40. package/dist/tools/handlers/export.d.ts.map +1 -1
  41. package/dist/tools/handlers/export.js +54 -4
  42. package/dist/tools/handlers/export.js.map +1 -1
  43. package/dist/tools/handlers.d.ts.map +1 -1
  44. package/dist/tools/handlers.js +61 -2
  45. package/dist/tools/handlers.js.map +1 -1
  46. package/dist/tools/lane.d.ts +4 -3
  47. package/dist/tools/lane.d.ts.map +1 -1
  48. package/dist/tools/lane.js +31 -5
  49. package/dist/tools/lane.js.map +1 -1
  50. package/dist/tools/scene-cache-export.d.ts +52 -0
  51. package/dist/tools/scene-cache-export.d.ts.map +1 -0
  52. package/dist/tools/scene-cache-export.js +227 -0
  53. package/dist/tools/scene-cache-export.js.map +1 -0
  54. package/dist/tools/shape-params.d.ts +2 -2
  55. package/dist/tools/shape-params.js +2 -2
  56. package/dist/tools/stick-vocabulary.d.ts +1 -1
  57. package/dist/tools/stick-vocabulary.js +1 -1
  58. package/dist/types/code-generator.d.ts +1 -0
  59. package/dist/types/code-generator.d.ts.map +1 -1
  60. package/dist/types/code-generator.js +437 -249
  61. package/dist/types/code-generator.js.map +1 -1
  62. package/dist/types/schemas.d.ts +211 -199
  63. package/dist/types/schemas.d.ts.map +1 -1
  64. package/dist/types/schemas.js +7 -1
  65. package/dist/types/schemas.js.map +1 -1
  66. package/manifest.json +1 -1
  67. package/package.json +1 -1
@@ -7,11 +7,18 @@
7
7
  import { compileChoreography } from '../tools/choreograph.js';
8
8
  import { compileBatchTimelines, emitTimelineOpBody } from '../tools/timeline.js';
9
9
  import { normalizeSvgSize } from '../utils/svg-size.js';
10
- import { WORLD3D_COLOR_PATHS, PATTERN_KINDS } from '../tools/engine-lists.js';
10
+ import { WORLD3D_COLOR_PATHS, PATTERN_KINDS, EXPRESSION_FUNCTION_ARITY, EXPRESSION_CONSTANTS, EXPRESSION_REJECTED, RELATION_PARAM_KEYS } from '../tools/engine-lists.js';
11
+ import { expressionGrammarText } from '../tools/expression-grammar.js';
11
12
  import { REQUIRED_ENGINE_METHODS, OPTIONAL_ENGINE_METHODS } from '../tools/engine-methods.js';
12
13
  import { NORMALIZE_PARAM_READS, MODIFY_CHANGE_READS } from '../tools/shape-params.js';
13
14
  import { CREATE_KNOWN_KEYS } from '../tools/create-known-keys.js';
14
15
  import { jsLiteral } from '../utils/js-literal.js';
16
+ /** A value spliced into a generated `//` comment, on one line: a line break in caller text would end the comment and run the rest as code. */
17
+ /** How many relations pinepaper_lint_scene lists back; past it the result says how many there are. */
18
+ const RELATIONS_READBACK_CAP = 200;
19
+ function oneLine(v) {
20
+ return String(v).replace(/[\r\n\u2028\u2029]+/g, ' ');
21
+ }
15
22
  import { generateP5DrawCode } from '../tools/p5-compat/p5-helpers.js';
16
23
  import { expandBatchMacros } from '../tools/batch-macros.js';
17
24
  import { CreateItemInputSchema, ScreenSpaceSchema, StepTimingSchema, CounterSchema, ImportMotionCaptureInputSchema, ModifyItemInputSchema, AddRelationInputSchema, AnimateItemInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, CreateSceneInputSchema,
@@ -78,7 +85,7 @@ function normalizeGradientSpec(v, key) {
78
85
  */
79
86
  function generateGradientCode(gradient, itemVarName, position, propertyName = 'fillColor') {
80
87
  const { type, stops, origin, destination } = gradient;
81
- const stopsCode = stops.map(s => `['${s.color}', ${s.offset}]`).join(', ');
88
+ const stopsCode = stops.map(s => `[${jsLiteral(s.color)}, ${s.offset}]`).join(', ');
82
89
  if (type === 'radial') {
83
90
  // A RADIAL CORE BELONGS ON THE ITEM, NOT ON THE NUMBER ASKED FOR (17
84
91
  // RADIALOFFSET). The origin was the REQUESTED position and the radius a
@@ -126,7 +133,7 @@ ${itemVarName}.${propertyName} = {
126
133
  function generateShadowCode(itemVarName, shadowColor, shadowBlur, shadowOffset) {
127
134
  const lines = [];
128
135
  if (shadowColor) {
129
- lines.push(`${itemVarName}.shadowColor = '${shadowColor}';`);
136
+ lines.push(`${itemVarName}.shadowColor = ${jsLiteral(shadowColor)};`);
130
137
  }
131
138
  if (shadowBlur !== undefined) {
132
139
  lines.push(`${itemVarName}.shadowBlur = ${shadowBlur};`);
@@ -140,7 +147,7 @@ function generateShadowCode(itemVarName, shadowColor, shadowBlur, shadowOffset)
140
147
  * Generate blend mode code for an item
141
148
  */
142
149
  function generateBlendModeCode(itemVarName, blendMode) {
143
- return `${itemVarName}.blendMode = '${blendMode}';`;
150
+ return `${itemVarName}.blendMode = ${jsLiteral(blendMode)};`;
144
151
  }
145
152
  // =============================================================================
146
153
  // CODE TEMPLATES
@@ -155,7 +162,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
155
162
  // break those later calls: refused here, by name, before anything is made.
156
163
  if (typeof properties.id === 'string' && UNSAFE_ID_CHARS.test(properties.id)) {
157
164
  return `
158
- // Create ${itemType} — refused: an id later calls could not address
165
+ // Create ${oneLine(itemType)} — refused: an id later calls could not address
159
166
  ({ success: false, error: ${jsLiteral(`properties.id ${JSON.stringify(properties.id)} has a quote, backslash or line break, so later calls could not address the item. Nothing was made. Use letters, digits, spaces, - and _ (e.g. "bobs-hat").`)} });`.trim();
160
167
  }
161
168
  // Colour objects are gradient specs: normalised, or refused by name.
@@ -165,7 +172,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
165
172
  const g = normalizeGradientSpec(v, key);
166
173
  if ('error' in g) {
167
174
  return `
168
- // Create ${itemType} — refused: an unreadable gradient would draw nothing
175
+ // Create ${oneLine(itemType)} — refused: an unreadable gradient would draw nothing
169
176
  ({ success: false, error: ${JSON.stringify(g.error)} });`.trim();
170
177
  }
171
178
  properties = { ...properties, [key]: g.gradient };
@@ -274,7 +281,7 @@ function generateCreateItemCode(itemType, position, properties, dataFlags, posit
274
281
  if (params.opacity != null)
275
282
  plate.opacity = params.opacity;
276
283
  return `
277
- // ${itemType} — a render-time surface: a plate tagged with what to draw over it.
284
+ // ${oneLine(itemType)} — a render-time surface: a plate tagged with what to draw over it.
278
285
  const item = app.create('rectangle', ${JSON.stringify(plate, null, 2)});
279
286
  if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.data.renderParams = ${JSON.stringify(params)}; }
280
287
  // The snippet's value is its last statement's, which was the assignment above:
@@ -284,7 +291,7 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
284
291
  // and its value is lost.
285
292
  ((item && item.data)
286
293
  ? { itemId: item.data.registryId, type: ${JSON.stringify(itemType)}, position: { x: ${position.x}, y: ${position.y} } }
287
- : { success: false, error: 'the plate for this ${itemType} was not created.' });`;
294
+ : { success: false, error: 'the plate for this ' + ${jsLiteral(itemType)} + ' was not created.' });`;
288
295
  }
289
296
  // AN UNKNOWN ANCHOR IS INVISIBLE OVER MCP.
290
297
  //
@@ -299,7 +306,7 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
299
306
  const normalized = String(anchorGiven).toLowerCase().replace(/[\s_]+/g, '-');
300
307
  if (!ANCHORS.includes(normalized)) {
301
308
  return `
302
- // Create ${itemType} — refused, rather than silently centred
309
+ // Create ${oneLine(itemType)} — refused, rather than silently centred
303
310
  ({ success: false, error: ${JSON.stringify(`anchor ${JSON.stringify(String(anchorGiven))} is not one of ${ANCHORS.slice(0, 5).join(', ')}. `
304
311
  + 'The engine would leave x/y as the CENTRE and say so only in a console this tool cannot read, '
305
312
  + 'so the item would land half its own size away with nothing to explain it.')} });`.trim();
@@ -331,8 +338,8 @@ if (item && item.data) { item.data.renderAs = ${JSON.stringify(itemType)}; item.
331
338
  : !CREATE_KNOWN_KEYS.has(k) || textOnly(k));
332
339
  // Build the code
333
340
  let code = `
334
- // Create ${itemType} item
335
- const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
341
+ // Create ${oneLine(itemType)} item
342
+ const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(params, null, 2)});`;
336
343
  // Add gradient support for colors
337
344
  if (color && isGradient(color)) {
338
345
  code += generateGradientCode(color, 'item', position, 'fillColor');
@@ -413,7 +420,7 @@ const item = app.create('${itemType}', ${JSON.stringify(params, null, 2)});`;
413
420
  opts.countdownTarget = countdownTarget;
414
421
  if (countdownEndText)
415
422
  opts.countdownEndText = countdownEndText;
416
- code += `\nif (app.setDynamicContent) app.setDynamicContent(item, '${contentType}', ${JSON.stringify(opts)});`;
423
+ code += `\nif (app.setDynamicContent) app.setDynamicContent(item, ${jsLiteral(contentType)}, ${JSON.stringify(opts)});`;
417
424
  }
418
425
  const loadsFont = itemType === 'text' && typeof properties.fontFamily === 'string' && properties.fontFamily.trim() !== '';
419
426
  // A LINE WITH `from` IS DRAWN WHERE from AND to SAY (issue #29 O8). They are
@@ -435,7 +442,7 @@ const itemId = item.data.registryId;
435
442
  app.historyManager.saveState();
436
443
 
437
444
  // Return item info
438
- ({ itemId, type: '${itemType}'${outlineOnly ? `, fill: ${JSON.stringify(OUTLINE_NOTE)}` : ''}${textFit ? ', textFit: __textFit' : ''}${textDirection ? ', direction: __direction' : ''}${textTabular ? ', tabularFigures: __tabular' : ''}${createFillOpacity ? ', fillOpacity: __fillOpacity' : ''}${textStrokePos ? ', strokePosition: __strokePos' : ''}, position: ${reportActualPosition ? `(item.position ? { x: item.position.x, y: item.position.y } : null)` : `{ x: ${position.x}, y: ${position.y} }`}${hasLifetime ? ', lifetime: __lifetime' : ''}${loadsFont ? ', ...(__font ? { font: __font } : {})' : ''}${ignored.length > 0
445
+ ({ itemId, type: ${jsLiteral(itemType)}${outlineOnly ? `, fill: ${JSON.stringify(OUTLINE_NOTE)}` : ''}${textFit ? ', textFit: __textFit' : ''}${textDirection ? ', direction: __direction' : ''}${textTabular ? ', tabularFigures: __tabular' : ''}${createFillOpacity ? ', fillOpacity: __fillOpacity' : ''}${textStrokePos ? ', strokePosition: __strokePos' : ''}, position: ${reportActualPosition ? `(item.position ? { x: item.position.x, y: item.position.y } : null)` : `{ x: ${position.x}, y: ${position.y} }`}${hasLifetime ? ', lifetime: __lifetime' : ''}${loadsFont ? ', ...(__font ? { font: __font } : {})' : ''}${ignored.length > 0
439
446
  ? `, ignoredProperties: ${JSON.stringify(ignored)}` : ''}${warnings.length > 0 ? `, warning: ${JSON.stringify(warnings.join(' '))}` : ''} });`;
440
447
  // Loading a font is async, and it has to happen BEFORE create() measures the
441
448
  // text — so only this case becomes an async IIFE; every other create stays a
@@ -1003,7 +1010,7 @@ if (entry && entry.item && app.removeDynamicContent) app.removeDynamicContent(en
1003
1010
  opts.countdownEndText = countdownEndText;
1004
1011
  code += `
1005
1012
  const entry = app.itemRegistry.get(${ID});
1006
- if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item, '${contentType}', ${jsLiteral(opts)});`;
1013
+ if (entry && entry.item && app.setDynamicContent) app.setDynamicContent(entry.item, ${jsLiteral(contentType)}, ${jsLiteral(opts)});`;
1007
1014
  }
1008
1015
  }
1009
1016
  // AN AUDIO ITEM'S LEVEL IS NOT AN ITEM PROPERTY. modifyItem applies changes
@@ -1221,6 +1228,8 @@ export function relationSpecProblem(relationType, params) {
1221
1228
  const p = (params && typeof params === 'object' ? params : {});
1222
1229
  if (relationType === 'on_event_fire_if')
1223
1230
  return fireIfProblem(p);
1231
+ if (relationType === 'time_expression' && typeof p.expression === 'string')
1232
+ return expressionProblem(p.expression);
1224
1233
  if (typeof relationType === 'string' && VISIBILITY_REACTION.test(relationType))
1225
1234
  return visibilityProblem(relationType, p);
1226
1235
  if (relationType === 'on_event_add_relation' && (p.type === 'on_event_fire_if' || (typeof p.type === 'string' && VISIBILITY_REACTION.test(p.type)))) {
@@ -1230,6 +1239,110 @@ export function relationSpecProblem(relationType, params) {
1230
1239
  }
1231
1240
  return null;
1232
1241
  }
1242
+ /**
1243
+ * A time_expression formula outside the engine's grammar parses to nothing, and the relation then does nothing with
1244
+ * success reported (#39: Math.sin(...) from JavaScript habit, no motion). Names are checked against the grammar the
1245
+ * engine publishes (engine-lists, generated from expr-to-ir.js); syntax is the engine's to judge.
1246
+ */
1247
+ function expressionProblem(expr) {
1248
+ const allowed = new Set([...Object.keys(EXPRESSION_FUNCTION_ARITY), ...EXPRESSION_CONSTANTS, 't', 'v']);
1249
+ const grammar = expressionGrammarText();
1250
+ for (let i = 0; i < expr.length;) {
1251
+ const c = expr[i];
1252
+ if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(expr[i + 1] ?? ''))) {
1253
+ i++;
1254
+ while (i < expr.length && /[0-9.]/.test(expr[i]))
1255
+ i++;
1256
+ if (/[eE]/.test(expr[i] ?? '') && /[0-9+-]/.test(expr[i + 1] ?? '')) {
1257
+ i += 2;
1258
+ while (i < expr.length && /[0-9]/.test(expr[i]))
1259
+ i++;
1260
+ }
1261
+ continue;
1262
+ }
1263
+ if (/[A-Za-z_]/.test(c)) {
1264
+ let j = i + 1;
1265
+ while (j < expr.length && /[A-Za-z0-9_]/.test(expr[j]))
1266
+ j++;
1267
+ const name = expr.slice(i, j);
1268
+ if (expr[j] === '.') {
1269
+ const member = /^[A-Za-z_][A-Za-z0-9_]*/.exec(expr.slice(j + 1))?.[0] ?? '';
1270
+ return `expression ${JSON.stringify(expr)} uses ${name}.${member}, and the formula is not JavaScript: it would parse to nothing and the item would not move. Write ${member || 'the function'}(…) without ${name}. The grammar: ${grammar}.`;
1271
+ }
1272
+ if (EXPRESSION_REJECTED.includes(name)) {
1273
+ return `expression ${JSON.stringify(expr)} uses ${name}(), which the engine refuses: a formula must give the same value at the same time in every export. Use noise(t) for a varying value that repeats.`;
1274
+ }
1275
+ const arity = EXPRESSION_FUNCTION_ARITY[name];
1276
+ if (arity !== undefined) {
1277
+ // Count the call's top-level arguments: a wrong count parses to nothing as well (min(1, 2, 3)).
1278
+ let k = j;
1279
+ while (expr[k] === ' ')
1280
+ k++;
1281
+ if (expr[k] === '(') {
1282
+ let depth = 0, args = 0, any = false;
1283
+ for (let q = k; q < expr.length; q++) {
1284
+ const ch = expr[q];
1285
+ if (ch === '(') {
1286
+ depth++;
1287
+ continue;
1288
+ }
1289
+ if (ch === ')') {
1290
+ depth--;
1291
+ if (depth === 0)
1292
+ break;
1293
+ continue;
1294
+ }
1295
+ if (depth === 1 && ch === ',')
1296
+ args++;
1297
+ if (depth >= 1 && ch.trim())
1298
+ any = true;
1299
+ }
1300
+ const given = any ? args + 1 : 0;
1301
+ if (given !== arity)
1302
+ return `expression ${JSON.stringify(expr)} calls ${name} with ${given} argument${given === 1 ? '' : 's'}; it takes ${arity}, and any other count parses to nothing, so the item would not move.`;
1303
+ }
1304
+ }
1305
+ if (!allowed.has(name)) {
1306
+ const near = [...allowed].find((a) => a.toLowerCase() === name.toLowerCase());
1307
+ return `expression ${JSON.stringify(expr)} names ${name}, which the formula grammar does not have, so it would parse to nothing and the item would not move.${near ? ` Did you mean ${near}?` : ''} The grammar: ${grammar}.`;
1308
+ }
1309
+ i = j;
1310
+ continue;
1311
+ }
1312
+ i++;
1313
+ }
1314
+ return null;
1315
+ }
1316
+ /**
1317
+ * A relationOptions key the rule never reads, said instead of accepted in silence. The list is the engine's own
1318
+ * (RELATION_PARAM_KEYS: what each rule declares or reads); a rule without one (custom, event reactions) gets no
1319
+ * warning. `injected` are keys the tool added itself (a name pattern's index and count).
1320
+ */
1321
+ /** Levenshtein distance, for a "did you mean" on a misspelt key. */
1322
+ function editDistance(a, b) {
1323
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
1324
+ for (let i = 1; i <= a.length; i++) {
1325
+ let prev = row[0];
1326
+ row[0] = i;
1327
+ for (let j = 1; j <= b.length; j++) {
1328
+ const t = row[j];
1329
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1330
+ prev = t;
1331
+ }
1332
+ }
1333
+ return row[b.length];
1334
+ }
1335
+ export function relationOptionWarning(relationType, params, injected = []) {
1336
+ const keys = typeof relationType === 'string' ? RELATION_PARAM_KEYS[relationType] : undefined;
1337
+ if (!keys || !params || typeof params !== 'object')
1338
+ return null;
1339
+ const unknown = Object.keys(params).filter((k) => !keys.includes(k) && !injected.includes(k));
1340
+ if (!unknown.length)
1341
+ return null;
1342
+ const norm = (x) => x.replace(/_/g, '').toLowerCase();
1343
+ const hint = unknown.map((k) => keys.find((x) => norm(x) === norm(k)) ?? keys.find((x) => editDistance(norm(x), norm(k)) <= (k.length > 5 ? 2 : 1))).map((x, i) => (x ? `${unknown[i]} → ${x}?` : '')).filter(Boolean);
1344
+ return `${relationType} does not read ${unknown.join(', ')}, so ${unknown.length > 1 ? 'they were' : 'it was'} ignored${hint.length ? ` (${hint.join('; ')})` : ''}. ${relationType} reads: ${keys.join(', ')}.`;
1345
+ }
1233
1346
  const VISIBILITY_REACTION = /^on_(event|enter|exit)_set_visibility$/;
1234
1347
  /**
1235
1348
  * A visibility reaction reads params.visible and nothing else: absent means show. `value`, which the other set_*
@@ -1249,23 +1362,23 @@ function generateAddRelationCode(sourceId, targetId, relationType, params) {
1249
1362
  // an item, the camera, a cursor target — is asked first here, after the same
1250
1363
  // '#id' forgiveness the engine applies.
1251
1364
  return `
1252
- // Add ${relationType} relation: ${sourceId} -> ${targetId}
1365
+ // Add ${oneLine(relationType)} relation: ${oneLine(sourceId)} -> ${oneLine(targetId)}
1253
1366
  (function() {
1254
1367
  const norm = function(id) { return typeof app._resolveRelationEndpoint === 'function' ? app._resolveRelationEndpoint(id) : id; };
1255
1368
  if (typeof app._isKnownRelationEndpoint === 'function') {
1256
1369
  const bad = [${JSON.stringify(sourceId)}, ${JSON.stringify(targetId)}].filter(function(id) { return !app._isKnownRelationEndpoint(norm(id)); });
1257
1370
  if (bad.length) {
1258
- return { success: false, relationType: '${relationType}', error: bad.map(function(id) { return JSON.stringify(id); }).join(' and ')
1371
+ return { success: false, relationType: ${jsLiteral(relationType)}, error: bad.map(function(id) { return JSON.stringify(id); }).join(' and ')
1259
1372
  + ' ' + (bad.length > 1 ? 'are not items' : 'is not an item') + ' on the canvas, so the relation would be stored and skipped every frame. Check the id (pinepaper_get_items lists them).' };
1260
1373
  }
1261
1374
  }
1262
- const success = app.addRelation('${sourceId}', '${targetId}', '${relationType}', ${JSON.stringify(params, null, 2)});
1375
+ const success = app.addRelation(${jsLiteral(sourceId)}, ${jsLiteral(targetId)}, ${jsLiteral(relationType)}, ${JSON.stringify(params, null, 2)});
1263
1376
  if (!success) {
1264
1377
  const __why = ${relationRefusalReasonJs(jsLiteral(relationType))};
1265
- return { success: false, relationType: '${relationType}', error: 'the studio refused the ${relationType} relation between ${sourceId} and ${targetId}' + (__why ? ': ' + __why : '.') };
1378
+ return { success: false, relationType: ${jsLiteral(relationType)}, error: 'the studio refused the ' + ${jsLiteral(relationType)} + ' relation between ' + ${jsLiteral(sourceId)} + ' and ' + ${jsLiteral(targetId)} + (__why ? ': ' + __why : '.') };
1266
1379
  }
1267
1380
  app.historyManager.saveState();
1268
- return { success: true, sourceId: '${sourceId}', targetId: '${targetId}', relationType: '${relationType}' };
1381
+ return { success: true, sourceId: ${jsLiteral(sourceId)}, targetId: ${jsLiteral(targetId)}, relationType: ${jsLiteral(relationType)}${(() => { const w = relationOptionWarning(relationType, params); return w ? `, warning: ${jsLiteral(w)}` : ''; })()} };
1269
1382
  })();
1270
1383
  `.trim();
1271
1384
  }
@@ -1423,7 +1536,7 @@ function generateMovesAlongPathCode(sourceId, targetId, raw) {
1423
1536
  const hasDuration = typeof duration === 'number' && duration > 0;
1424
1537
  const hasDelay = typeof delay === 'number' && delay > 0;
1425
1538
  return `
1426
- // moves_along_path: ${sourceId}${ref ? ` along ${ref}` : ''}
1539
+ // moves_along_path: ${oneLine(sourceId)}${ref ? ` along ${oneLine(ref)}` : ''}
1427
1540
  (function() {
1428
1541
  const params = ${JSON.stringify(params)};
1429
1542
  const hasPoints = Array.isArray(params.path) && params.path.length >= 2;
@@ -1537,14 +1650,14 @@ function aliasGeneratorParams(name, params) {
1537
1650
  * Template for removing relations
1538
1651
  */
1539
1652
  function generateRemoveRelationCode(sourceId, targetId, relationType) {
1540
- const relationArg = relationType ? `'${relationType}'` : 'undefined';
1653
+ const relationArg = relationType ? jsLiteral(relationType) : 'undefined';
1541
1654
  return `
1542
- // Remove relation: ${sourceId} -> ${targetId}${relationType ? ` (${relationType})` : ''}
1543
- app.removeRelation('${sourceId}', '${targetId}', ${relationArg});
1655
+ // Remove relation: ${oneLine(sourceId)} -> ${oneLine(targetId)}${relationType ? ` (${oneLine(relationType)})` : ''}
1656
+ app.removeRelation(${jsLiteral(sourceId)}, ${jsLiteral(targetId)}, ${relationArg});
1544
1657
  app.historyManager.saveState();
1545
1658
 
1546
1659
  // Return success
1547
- ({ success: true, sourceId: '${sourceId}', targetId: '${targetId}' });
1660
+ ({ success: true, sourceId: ${jsLiteral(sourceId)}, targetId: ${jsLiteral(targetId)} });
1548
1661
  `.trim();
1549
1662
  }
1550
1663
  /**
@@ -1553,7 +1666,7 @@ app.historyManager.saveState();
1553
1666
  function generateQueryRelationsCode(itemId, relationType, direction = 'outgoing') {
1554
1667
  const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
1555
1668
  if (direction === 'outgoing') {
1556
- const typeArg = relationType ? `'${relationType}'` : 'undefined';
1669
+ const typeArg = relationType ? jsLiteral(relationType) : 'undefined';
1557
1670
  return `
1558
1671
  // Query outgoing relations for ${ID}
1559
1672
  const relations = app.getRelations(${ID}, ${typeArg});
@@ -1574,7 +1687,7 @@ const formatted = relations.map(r => ({
1574
1687
  `.trim();
1575
1688
  }
1576
1689
  else {
1577
- const typeArg = relationType ? `'${relationType}'` : 'undefined';
1690
+ const typeArg = relationType ? jsLiteral(relationType) : 'undefined';
1578
1691
  return `
1579
1692
  // Query incoming relations for ${ID}
1580
1693
  const results = app.queryByRelationTarget(${ID}, ${typeArg});
@@ -1586,7 +1699,7 @@ const relations = results.map(r => ({
1586
1699
  // queryByTarget returns the real relation name on each result. Hardcoding
1587
1700
  // the filter string labelled every edge 'unknown' on an unfiltered query —
1588
1701
  // which is exactly the query you make when exploring an unfamiliar graph.
1589
- relationType: r.relation || ${relationType ? `'${relationType}'` : "'unknown'"},
1702
+ relationType: r.relation || ${relationType ? jsLiteral(relationType) : "'unknown'"},
1590
1703
  params: r.params || {}
1591
1704
  }));
1592
1705
 
@@ -1602,13 +1715,13 @@ function generateAnimateCode(itemId, animationType, speed, intensity, delay) {
1602
1715
  const extra = (intensity !== undefined ? `,\n animationIntensity: ${intensity}` : '') +
1603
1716
  (delay !== undefined ? `,\n animationDelay: ${delay}` : '');
1604
1717
  return `
1605
- // Apply ${animationType} animation to ${ID}
1718
+ // Apply ${oneLine(animationType)} animation to ${ID}
1606
1719
  const item = app.getItemById(${ID});
1607
1720
  if (!item) {
1608
1721
  throw new Error('Item not found: ' + ${ID});
1609
1722
  }
1610
1723
  app.animate(item, {
1611
- animationType: '${animationType}',
1724
+ animationType: ${jsLiteral(animationType)},
1612
1725
  animationSpeed: ${speed}${extra}
1613
1726
  });
1614
1727
 
@@ -1627,7 +1740,7 @@ app.animate(item, {
1627
1740
  known: r.known,
1628
1741
  };
1629
1742
  }
1630
- return { success: true, itemId: ${ID}, animationType: '${animationType}' };
1743
+ return { success: true, itemId: ${ID}, animationType: ${jsLiteral(animationType)} };
1631
1744
  })();
1632
1745
  `.trim();
1633
1746
  }
@@ -1868,7 +1981,7 @@ function generateExecuteGeneratorCode(generatorName, params, region) {
1868
1981
  ? ` in region ${region.width}×${region.height} @ (${region.x},${region.y})`
1869
1982
  : ` in the shape of ${region.shape.itemId}`;
1870
1983
  return `
1871
- // Execute ${generatorName} generator${where}
1984
+ // Execute ${oneLine(generatorName)} generator${oneLine(where)}
1872
1985
  (async function() {${emitGeneratorRun(generatorName, JSON.stringify(params, null, 2), region)}
1873
1986
  })();
1874
1987
  `.trim();
@@ -1895,9 +2008,9 @@ function emitGeneratorRun(generatorName, paramsJson, region) {
1895
2008
  if (!__si) return { success: false, error: ${JSON.stringify(`region.shape names no item "${itemId}" — nothing was drawn. List items with pinepaper_get_items.`)} };
1896
2009
  if (__si.className !== 'Path' && __si.className !== 'CompoundPath') return { success: false, error: ${JSON.stringify(`region.shape item "${itemId}" is a `)} + __si.className + ', and a clip needs a path (a shape, not text, a group or an image) — nothing was drawn.' };
1897
2010
  }` : ''}
1898
- await app.executeGenerator('${generatorName}', ${paramsJson}, ${options});${shaped ? `
2011
+ await app.executeGenerator(${jsLiteral(generatorName)}, ${paramsJson}, ${options});${shaped ? `
1899
2012
  const __shapes = !!(app.generatorRegistry && typeof app.generatorRegistry._regionClipPath === 'function');` : ''}
1900
- return { success: true, generator: '${generatorName}'${region ? `, region: ${JSON.stringify(region)}` : ''}${shaped ? `, ...(__shapes ? {} : { regionShapeApplied: false, warning: ${JSON.stringify(`this studio does not clip generator regions to a shape, so ${typeof region.shape === 'object' ? 'the region had no box and the generator drew across the whole canvas' : `the ${region.shape} was drawn as a rectangle`}. It needs an engine with region shapes.`)} })` : ''} };`;
2013
+ return { success: true, generator: ${jsLiteral(generatorName)}${region ? `, region: ${JSON.stringify(region)}` : ''}${shaped ? `, ...(__shapes ? {} : { regionShapeApplied: false, warning: ${JSON.stringify(`this studio does not clip generator regions to a shape, so ${typeof region.shape === 'object' ? 'the region had no box and the generator drew across the whole canvas' : `the ${region.shape} was drawn as a rectangle`}. It needs an engine with region shapes.`)} })` : ''} };`;
1901
2014
  }
1902
2015
  /**
1903
2016
  * Template for applying effects
@@ -1905,14 +2018,14 @@ function emitGeneratorRun(generatorName, paramsJson, region) {
1905
2018
  function generateApplyEffectCode(itemId, effectType, params) {
1906
2019
  const ID = jsLiteral(itemId); // caller text: a quoted literal, never spliced raw
1907
2020
  return `
1908
- // Apply ${effectType} effect to ${ID}
2021
+ // Apply ${oneLine(effectType)} effect to ${ID}
1909
2022
  const item = app.getItemById(${ID});
1910
2023
  if (!item) {
1911
2024
  throw new Error('Item not found: ' + ${ID});
1912
2025
  }
1913
- app.applyEffect(item, '${effectType}', ${JSON.stringify(params, null, 2)});
2026
+ app.applyEffect(item, ${jsLiteral(effectType)}, ${JSON.stringify(params, null, 2)});
1914
2027
 
1915
- ({ success: true, itemId: ${ID}, effectType: '${effectType}' });
2028
+ ({ success: true, itemId: ${ID}, effectType: ${jsLiteral(effectType)} });
1916
2029
  `.trim();
1917
2030
  }
1918
2031
  /**
@@ -1964,9 +2077,9 @@ function generateGetItemsCode(filter) {
1964
2077
  // conditions, not on the keys.
1965
2078
  const conditions = [];
1966
2079
  if (filter?.type)
1967
- conditions.push(`entry.type === '${filter.type}'`);
2080
+ conditions.push(`entry.type === ${jsLiteral(filter.type)}`);
1968
2081
  if (filter?.source)
1969
- conditions.push(`entry.source === '${filter.source}'`);
2082
+ conditions.push(`entry.source === ${jsLiteral(filter.source)}`);
1970
2083
  if (filter?.hasAnimation !== undefined) {
1971
2084
  // 'none' is what the engine stores on a still item, and !!'none' is true
1972
2085
  // (gate H5): animated means a real loop animation OR keyframes.
@@ -2163,8 +2276,8 @@ app.setPlaybackTime(${time || 0});
2163
2276
  function generateSetBackgroundColorCode(color) {
2164
2277
  return `
2165
2278
  // Set background color
2166
- app.setBackgroundColor('${color}');
2167
- ({ success: true, color: '${color}' });
2279
+ app.setBackgroundColor(${jsLiteral(color)});
2280
+ ({ success: true, color: ${jsLiteral(color)} });
2168
2281
  `.trim();
2169
2282
  }
2170
2283
  /**
@@ -2671,7 +2784,7 @@ ${body}
2671
2784
  } catch (e) { errors.push({ index: ${i}, error: (e && e.message) || String(e) }); }`;
2672
2785
  });
2673
2786
  return `
2674
- // Batch create ${items.length} items — create_item for each, one history save
2787
+ // Batch create ${oneLine(items.length)} items — create_item for each, one history save
2675
2788
  (async function() {
2676
2789
  const results = [];
2677
2790
  const errors = [];
@@ -2704,7 +2817,7 @@ function generateBatchModifyCode(modifications) {
2704
2817
  } catch (e) { skipped.push({ itemId: ${JSON.stringify(m.itemId)}, reason: (e && e.message) || String(e) }); }`;
2705
2818
  });
2706
2819
  return `
2707
- // Batch modify ${modifications.length} items — modify_item for each, one history save
2820
+ // Batch modify ${oneLine(modifications.length)} items — modify_item for each, one history save
2708
2821
  (async function() {
2709
2822
  let count = 0;
2710
2823
  const skipped = [];
@@ -2726,11 +2839,11 @@ ${blocks.join('\n')}
2726
2839
  */
2727
2840
  function generateCreateGridCode(cols, rows, strokeColor, strokeWidth, animated, waveSpeed, waveAmplitude) {
2728
2841
  return `
2729
- // Create grid with ${cols} columns and ${rows} rows
2842
+ // Create grid with ${oneLine(cols)} columns and ${oneLine(rows)} rows
2730
2843
  const grid = app.createGrid({
2731
2844
  cols: ${cols},
2732
2845
  rows: ${rows},
2733
- strokeColor: '${strokeColor}',
2846
+ strokeColor: ${jsLiteral(strokeColor)},
2734
2847
  strokeWidth: ${strokeWidth},
2735
2848
  animated: ${animated},
2736
2849
  waveSpeed: ${waveSpeed},
@@ -3119,7 +3232,7 @@ export class PinePaperCodeGenerator {
3119
3232
  // than read as a single string, which would render "[object Object]".
3120
3233
  if (validated.presetId) {
3121
3234
  return `
3122
- // Adopt relation preset "${validated.presetId}" on ${validated.sourceId}
3235
+ // Adopt relation preset ${jsLiteral(validated.presetId)} on ${jsLiteral(validated.sourceId)}
3123
3236
  (async function() {
3124
3237
  if (typeof app.applyRelationPreset !== 'function') { return { success: false, error: 'app.applyRelationPreset unavailable — update FxTool' }; }
3125
3238
  const r = await app.applyRelationPreset(${JSON.stringify(validated.sourceId)}, ${JSON.stringify(validated.presetId)}, ${JSON.stringify(validated.presetValues ?? {})});
@@ -3149,7 +3262,7 @@ export class PinePaperCodeGenerator {
3149
3262
  if (validated.targetId)
3150
3263
  return code;
3151
3264
  return code
3152
- .replace(`return { success: true, sourceId: '${validated.sourceId}', targetId: '${validated.sourceId}',`, `return { success: true, sourceId: '${validated.sourceId}', targetId: null,`)
3265
+ .replace(`return { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: ${jsLiteral(validated.sourceId)},`, `return { success: true, sourceId: ${jsLiteral(validated.sourceId)}, targetId: null,`)
3153
3266
  .replace(`relation between ${validated.sourceId} and ${validated.sourceId}.`, `relation on ${validated.sourceId} (no target).`);
3154
3267
  }
3155
3268
  /**
@@ -3313,7 +3426,7 @@ ${asReturningBody(code)}
3313
3426
  : `{ item: app.importSVG(${varName}), changes: [] }`;
3314
3427
  if (url) {
3315
3428
  return `
3316
- // Import SVG from URL${source === 'figma' ? ' (Figma-normalised)' : ''}
3429
+ // Import SVG from URL${oneLine(source === 'figma' ? ' (Figma-normalised)' : '')}
3317
3430
  // WRAPPED, because these were bare top-level statements with an await. That
3318
3431
  // parses under the governor, which builds an AsyncFunction, and is a SYNTAX
3319
3432
  // ERROR on the eval fallback — so importing an SVG by URL never ran at all on
@@ -3322,12 +3435,12 @@ ${asReturningBody(code)}
3322
3435
  (async function() {
3323
3436
  let response;
3324
3437
  try {
3325
- response = await fetch('${url}');
3438
+ response = await fetch(${jsLiteral(url)});
3326
3439
  } catch (netErr) {
3327
- return { success: false, error: 'could not reach ${url} — ' + ((netErr && netErr.message) || 'network request failed') };
3440
+ return { success: false, error: 'could not reach ' + ${jsLiteral(url)} + ' — ' + ((netErr && netErr.message) || 'network request failed') };
3328
3441
  }
3329
3442
  if (!response.ok) {
3330
- return { success: false, error: 'the server refused ${url} — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
3443
+ return { success: false, error: 'the server refused ' + ${jsLiteral(url)} + ' — HTTP ' + response.status + ' ' + (response.statusText || ''), status: response.status };
3331
3444
  }
3332
3445
  let svgText = await response.text();
3333
3446
  ${INLINE_REMOTE_IMAGES}
@@ -3336,7 +3449,7 @@ ${INLINE_REMOTE_IMAGES}
3336
3449
  const _r = ${importExpr('svgText')};
3337
3450
  const imported = _r.item;
3338
3451
  if (!imported) {
3339
- return { success: false, error: 'the response from ${url} did not parse as SVG. Check the URL serves the file itself rather than a page around it.' };
3452
+ return { success: false, error: 'the response from ' + ${jsLiteral(url)} + ' did not parse as SVG. Check the URL serves the file itself rather than a page around it.' };
3340
3453
  }
3341
3454
  imported.position = new paper.Point(${position.x}, ${position.y});
3342
3455
  imported.scale(${scale});
@@ -3360,7 +3473,7 @@ ${INLINE_REMOTE_IMAGES}
3360
3473
  // Escape the SVG string for embedding in code
3361
3474
  const escapedSvg = svgString.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
3362
3475
  return `
3363
- // Import SVG string${source === 'figma' ? ' (Figma-normalised)' : ''}
3476
+ // Import SVG string${oneLine(source === 'figma' ? ' (Figma-normalised)' : '')}
3364
3477
  (async function() {
3365
3478
  let svgString = \`${escapedSvg}\`;
3366
3479
  ${INLINE_REMOTE_IMAGES}
@@ -3542,9 +3655,9 @@ throw new Error('Either svgString or url must be provided');
3542
3655
  */
3543
3656
  generateAddFilter(filterType, params = {}) {
3544
3657
  return `
3545
- // Add ${filterType} filter
3546
- const filterId = app.filterSystem.addFilter('${filterType}', ${JSON.stringify(params, null, 2)});
3547
- ({ success: true, filterId, filterType: '${filterType}' });
3658
+ // Add ${oneLine(filterType)} filter
3659
+ const filterId = app.filterSystem.addFilter(${jsLiteral(filterType)}, ${JSON.stringify(params, null, 2)});
3660
+ ({ success: true, filterId, filterType: ${jsLiteral(filterType)} });
3548
3661
  `.trim();
3549
3662
  }
3550
3663
  /**
@@ -3573,7 +3686,7 @@ const filterId = app.filterSystem.addFilter('${filterType}', ${JSON.stringify(pa
3573
3686
  return `
3574
3687
  // Create glossy 3D sphere effect
3575
3688
  const sphereGroup = new paper.Group({ parent: app.textItemGroup });
3576
- const baseColor = new paper.Color('${baseColor}');
3689
+ const baseColor = new paper.Color(${jsLiteral(baseColor)});
3577
3690
 
3578
3691
  ${castShadow ? `// Shadow underneath
3579
3692
  const shadow = new paper.Path.Ellipse({
@@ -3753,7 +3866,7 @@ paper.project.activeLayer.children.forEach(c => {
3753
3866
  const bg = new paper.Path.Rectangle({
3754
3867
  point: [0, 0],
3755
3868
  size: [paper.view.size.width, paper.view.size.height],
3756
- fillColor: '${backgroundColor}',
3869
+ fillColor: ${jsLiteral(backgroundColor)},
3757
3870
  name: 'background'
3758
3871
  });
3759
3872
  bg.sendToBack();
@@ -3763,7 +3876,7 @@ bg.sendToBack();
3763
3876
  if (backgroundGenerator) {
3764
3877
  codeParts.push(`
3765
3878
  // Execute background generator
3766
- const _gen = app.generatorRegistry?.get('${backgroundGenerator}');
3879
+ const _gen = app.generatorRegistry?.get(${jsLiteral(backgroundGenerator)});
3767
3880
  if (typeof _gen === 'function') {
3768
3881
  _gen();
3769
3882
  } else if (_gen && typeof _gen.generate === 'function') {
@@ -3795,20 +3908,20 @@ if (typeof _gen === 'function') {
3795
3908
  ...properties,
3796
3909
  };
3797
3910
  codeParts.push(`
3798
- // Create item: ${name}
3911
+ // Create item: ${oneLine(name)}
3799
3912
  (function() {
3800
- const item = app.create('${itemType}', ${JSON.stringify(sceneParams)});
3913
+ const item = app.create(${jsLiteral(itemType)}, ${JSON.stringify(sceneParams)});
3801
3914
  if (!item) {
3802
- results.items.push({ name: '${name}', error: 'app.create returned nothing for type ${itemType} — check the type and its properties.' });
3915
+ results.items.push({ name: ${jsLiteral(name)}, error: 'app.create returned nothing for type ' + ${jsLiteral(itemType)} + ' — check the type and its properties.' });
3803
3916
  return;
3804
3917
  }
3805
3918
  // create() registers and mints the id; only fall back for an older studio.
3806
3919
  const itemId = (item.data && item.data.id)
3807
3920
  ? item.data.id
3808
- : app.registerItem(item, '${itemType}', { source: 'mcp-scene' });
3921
+ : app.registerItem(item, ${jsLiteral(itemType)}, { source: 'mcp-scene' });
3809
3922
  if (item.bringToFront) item.bringToFront();
3810
- nameToId['${name}'] = itemId;
3811
- results.items.push({ name: '${name}', itemId, type: '${itemType}' });
3923
+ nameToId[${jsLiteral(name)}] = itemId;
3924
+ results.items.push({ name: ${jsLiteral(name)}, itemId, type: ${jsLiteral(itemType)} });
3812
3925
  })();`);
3813
3926
  }
3814
3927
  // Establish relations
@@ -3818,25 +3931,25 @@ if (typeof _gen === 'function') {
3818
3931
  const specProblem = relationSpecProblem(type, params);
3819
3932
  if (specProblem) {
3820
3933
  codeParts.push(`
3821
- // Refused relation: ${source} ${type} ${target}
3934
+ // Refused relation: ${oneLine(source)} ${oneLine(type)} ${oneLine(target)}
3822
3935
  (results.relationsRefused = results.relationsRefused || []).push(${JSON.stringify({ source, target, type, reason: specProblem })});
3823
3936
  results.success = false;
3824
3937
  results.error = 'refused ' + results.relationsRefused.length + ' relation(s): ' + results.relationsRefused.map(function(r) { return r.type + ' ' + r.source + ' -> ' + r.target + ' (' + r.reason + ')'; }).join('; ') + '. The items were created.';`);
3825
3938
  continue;
3826
3939
  }
3827
3940
  codeParts.push(`
3828
- // Add relation: ${source} ${type} ${target}
3941
+ // Add relation: ${oneLine(source)} ${oneLine(type)} ${oneLine(target)}
3829
3942
  (function() {
3830
- const sourceId = nameToId['${source}'];
3831
- const targetId = nameToId['${target}'];
3943
+ const sourceId = nameToId[${jsLiteral(source)}];
3944
+ const targetId = nameToId[${jsLiteral(target)}];
3832
3945
  if (sourceId && targetId) {
3833
3946
  const params = ${paramsStr};
3834
3947
  // A refused relation is not listed as made (owner rule 1).
3835
- if (app.addRelation(sourceId, targetId, '${type}', params)) {
3836
- results.relations.push({ source: '${source}', target: '${target}', type: '${type}' });
3948
+ if (app.addRelation(sourceId, targetId, ${jsLiteral(type)}, params)) {
3949
+ results.relations.push({ source: ${jsLiteral(source)}, target: ${jsLiteral(target)}, type: ${jsLiteral(type)} });
3837
3950
  } else {
3838
3951
  const __why = ${relationRefusalReasonJs(jsLiteral(type))};
3839
- (results.relationsRefused = results.relationsRefused || []).push({ source: '${source}', target: '${target}', type: '${type}', reason: __why || 'the studio refused it' });
3952
+ (results.relationsRefused = results.relationsRefused || []).push({ source: ${jsLiteral(source)}, target: ${jsLiteral(target)}, type: ${jsLiteral(type)}, reason: __why || 'the studio refused it' });
3840
3953
  results.success = false;
3841
3954
  results.error = 'the studio refused ' + results.relationsRefused.length + ' relation(s): ' + results.relationsRefused.map(function(r) { return r.type + ' ' + r.source + ' -> ' + r.target + ' (' + r.reason + ')'; }).join('; ') + '. The items were created.';
3842
3955
  }
@@ -3853,9 +3966,9 @@ results.error = 'refused ' + results.relationsRefused.length + ' relation(s): '
3853
3966
  ...(startTime !== undefined ? { animationDelay: startTime } : {}),
3854
3967
  });
3855
3968
  codeParts.push(`
3856
- // Apply animation: ${type} to ${target}
3969
+ // Apply animation: ${oneLine(type)} to ${oneLine(target)}
3857
3970
  (function() {
3858
- const targetId = nameToId['${target}'];
3971
+ const targetId = nameToId[${jsLiteral(target)}];
3859
3972
  if (targetId) {
3860
3973
  const targetItem = app.itemRegistry.get(targetId);
3861
3974
  if (targetItem) {
@@ -3865,8 +3978,8 @@ results.error = 'refused ' + results.relationsRefused.length + ' relation(s): '
3865
3978
  // loops with no end, so there is no duration to map and none is invented
3866
3979
  // — staggering the STARTS is what makes a scene read as choreographed
3867
3980
  // rather than everything moving at once from frame zero.
3868
- app.animate(targetItem, { animationType: '${type}', ...params });
3869
- results.animations.push({ target: '${target}', type: '${type}'${startTime !== undefined ? `, startTime: ${startTime}` : ''} });
3981
+ app.animate(targetItem, { animationType: ${jsLiteral(type)}, ...params });
3982
+ results.animations.push({ target: ${jsLiteral(target)}, type: ${jsLiteral(type)}${startTime !== undefined ? `, startTime: ${startTime}` : ''} });
3870
3983
  }
3871
3984
  }
3872
3985
  })();
@@ -3900,7 +4013,7 @@ return results;
3900
4013
  // ending in \ escapes the closing quote; 'x\\'; evil()' breaks out).
3901
4014
  const labelStr = label ? JSON.stringify(label) : 'null';
3902
4015
  return `
3903
- // Create diagram shape: ${shapeType}
4016
+ // Create diagram shape: ${oneLine(shapeType)}
3904
4017
  // POSITIONAL: createShape(shapeType, config). The type was being passed
3905
4018
  // INSIDE the config object, where nothing reads it.
3906
4019
  const shapeConfig = {
@@ -3911,12 +4024,12 @@ const shapeConfig = {
3911
4024
  style: ${styleStr}
3912
4025
  };
3913
4026
 
3914
- const shape = app.diagramSystem.createShape('${shapeType}', shapeConfig);
3915
- if (!shape) throw new Error('createShape returned nothing for shapeType "${shapeType}" — call pinepaper_get_diagram_shapes for the registered types.');
4027
+ const shape = app.diagramSystem.createShape(${jsLiteral(shapeType)}, shapeConfig);
4028
+ if (!shape) throw new Error('createShape returned nothing for shapeType ' + ${jsLiteral(shapeType)} + ' — call pinepaper_get_diagram_shapes for the registered types.');
3916
4029
  const itemId = shape.data?.registryId || shape.id;
3917
4030
  app.historyManager.saveState();
3918
4031
 
3919
- ({ itemId, shapeType: '${shapeType}', position: { x: ${position.x}, y: ${position.y} } });
4032
+ ({ itemId, shapeType: ${jsLiteral(shapeType)}, position: { x: ${position.x}, y: ${position.y} } });
3920
4033
  `.trim();
3921
4034
  }
3922
4035
  /**
@@ -3948,12 +4061,12 @@ app.historyManager.saveState();
3948
4061
  if (label)
3949
4062
  config.label = label;
3950
4063
  return `
3951
- // Connect items: ${sourceItemId} -> ${targetItemId}
3952
- const sourceItem = app.getItemById('${sourceItemId}');
3953
- const targetItem = app.getItemById('${targetItemId}');
4064
+ // Connect items: ${oneLine(sourceItemId)} -> ${oneLine(targetItemId)}
4065
+ const sourceItem = app.getItemById(${jsLiteral(sourceItemId)});
4066
+ const targetItem = app.getItemById(${jsLiteral(targetItemId)});
3954
4067
 
3955
- if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
3956
- if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
4068
+ if (!sourceItem) throw new Error('Source item not found: ' + ${jsLiteral(sourceItemId)});
4069
+ if (!targetItem) throw new Error('Target item not found: ' + ${jsLiteral(targetItemId)});
3957
4070
 
3958
4071
  const config = ${JSON.stringify(config, null, 2)};
3959
4072
  const connector = app.diagramSystem.connect(sourceItem, targetItem, config);
@@ -3971,8 +4084,8 @@ app.historyManager.saveState();
3971
4084
  // mismatch is REPORTED rather than left for a later call to trip over.
3972
4085
  ({
3973
4086
  connectorId,
3974
- sourceItemId: '${sourceItemId}',
3975
- targetItemId: '${targetItemId}',
4087
+ sourceItemId: ${jsLiteral(sourceItemId)},
4088
+ targetItemId: ${jsLiteral(targetItemId)},
3976
4089
  ...(${JSON.stringify(id ?? null)} && connectorId !== ${JSON.stringify(id ?? null)}
3977
4090
  ? { requestedId: ${JSON.stringify(id ?? null)},
3978
4091
  idHonoured: false,
@@ -3994,12 +4107,12 @@ app.historyManager.saveState();
3994
4107
  const merged = id ? { ...(config ?? {}), id } : config;
3995
4108
  const configStr = merged ? JSON.stringify(merged, null, 2) : '{}';
3996
4109
  return `
3997
- // Connect ports: ${sourceItemId}:${sourcePort} -> ${targetItemId}:${targetPort}
3998
- const sourceItem = app.getItemById('${sourceItemId}');
3999
- const targetItem = app.getItemById('${targetItemId}');
4110
+ // Connect ports: ${oneLine(sourceItemId)}:${oneLine(sourcePort)} -> ${oneLine(targetItemId)}:${oneLine(targetPort)}
4111
+ const sourceItem = app.getItemById(${jsLiteral(sourceItemId)});
4112
+ const targetItem = app.getItemById(${jsLiteral(targetItemId)});
4000
4113
 
4001
- if (!sourceItem) throw new Error('Source item not found: ${sourceItemId}');
4002
- if (!targetItem) throw new Error('Target item not found: ${targetItemId}');
4114
+ if (!sourceItem) throw new Error('Source item not found: ' + ${jsLiteral(sourceItemId)});
4115
+ if (!targetItem) throw new Error('Target item not found: ' + ${jsLiteral(targetItemId)});
4003
4116
 
4004
4117
  const config = ${configStr};
4005
4118
  // connectPorts(sourcePort, targetPort, config) takes PORT OBJECTS (gate C8,
@@ -4024,7 +4137,7 @@ if (!connector) { return { success: false, error: 'the diagram system did not cr
4024
4137
  const connectorId = connector.id || connector.data?.registryId;
4025
4138
  app.historyManager.saveState();
4026
4139
 
4027
- ({ connectorId, sourceItemId: '${sourceItemId}', sourcePort: '${sourcePort}', targetItemId: '${targetItemId}', targetPort: '${targetPort}' });
4140
+ ({ connectorId, sourceItemId: ${jsLiteral(sourceItemId)}, sourcePort: ${jsLiteral(sourcePort)}, targetItemId: ${jsLiteral(targetItemId)}, targetPort: ${jsLiteral(targetPort)} });
4028
4141
  `.trim();
4029
4142
  }
4030
4143
  /**
@@ -4042,22 +4155,22 @@ app.historyManager.saveState();
4042
4155
  if (ports !== undefined || count !== undefined) {
4043
4156
  const named = [ports !== undefined && 'ports', count !== undefined && 'count'].filter(Boolean).join(' and ');
4044
4157
  throw new Error(`add_ports cannot honour ${named}: this engine lays out a whole port SET by type `
4045
- + `(portType '${portType}'), with no per-port placement or count. Drop ${named}, or place `
4158
+ + `(portType ${jsLiteral(portType)}), with no per-port placement or count. Drop ${named}, or place `
4046
4159
  + 'individual connection points as items and join them with pinepaper_connect.');
4047
4160
  }
4048
4161
  return `
4049
- // Add ports to item: ${itemId}
4050
- const item = app.getItemById('${itemId}');
4051
- if (!item) throw new Error('Item not found: ${itemId}');
4162
+ // Add ports to item: ${oneLine(itemId)}
4163
+ const item = app.getItemById(${jsLiteral(itemId)});
4164
+ if (!item) throw new Error('Item not found: ' + ${jsLiteral(itemId)});
4052
4165
 
4053
4166
  // The second argument is a TYPE STRING, not a config object. Passing an
4054
4167
  // object made the engine iterate it as a list — "t.forEach is not a function".
4055
- const result = app.diagramSystem.addPorts(item, '${portType}');
4168
+ const result = app.diagramSystem.addPorts(item, ${jsLiteral(portType)});
4056
4169
  app.historyManager.saveState();
4057
4170
 
4058
4171
  // addPorts returns the ports it made; older builds returned a count object.
4059
4172
  const _added = Array.isArray(result) ? result.length : (result?.portsAdded ?? 0);
4060
- ({ itemId: '${itemId}', portsAdded: _added, portType: '${portType}' });
4173
+ ({ itemId: ${jsLiteral(itemId)}, portsAdded: _added, portType: ${jsLiteral(portType)} });
4061
4174
  `.trim();
4062
4175
  }
4063
4176
  /**
@@ -4069,7 +4182,7 @@ const _added = Array.isArray(result) ? result.length : (result?.portsAdded ?? 0)
4069
4182
  const itemIdsStr = itemIds ? JSON.stringify(itemIds) : 'null';
4070
4183
  const optionsStr = options ? JSON.stringify(options, null, 2) : '{}';
4071
4184
  return `
4072
- // Apply auto-layout: ${layoutType}
4185
+ // Apply auto-layout: ${oneLine(layoutType)}
4073
4186
  // Wrapped: applyLayout is async, and a bare top-level await only works on the
4074
4187
  // governed path. A paren-led IIFE also keeps the trailing-expression capture.
4075
4188
  (async function() {
@@ -4085,10 +4198,10 @@ const items = itemIds
4085
4198
  : (app.itemRegistry?.getAll?.() ?? []);
4086
4199
  if (!items.length) throw new Error('auto_layout found no items to lay out' + (itemIds ? ' for the given itemIds' : ' on the canvas'));
4087
4200
 
4088
- await app.diagramSystem.applyLayout(items, '${layoutType}', options);
4201
+ await app.diagramSystem.applyLayout(items, ${jsLiteral(layoutType)}, options);
4089
4202
  app.historyManager.saveState();
4090
4203
 
4091
- return { layoutType: '${layoutType}', itemsAffected: items.length, success: true };
4204
+ return { layoutType: ${jsLiteral(layoutType)}, itemsAffected: items.length, success: true };
4092
4205
  })();
4093
4206
  `.trim();
4094
4207
  }
@@ -4098,7 +4211,7 @@ return { layoutType: '${layoutType}', itemsAffected: items.length, success: true
4098
4211
  generateGetDiagramShapes(input) {
4099
4212
  const validated = GetDiagramShapesInputSchema.parse(input);
4100
4213
  const { category } = validated;
4101
- const categoryFilter = category ? `'${category}'` : 'null';
4214
+ const categoryFilter = category ? jsLiteral(category) : 'null';
4102
4215
  return `
4103
4216
  // Get available diagram shapes
4104
4217
  const category = ${categoryFilter};
@@ -4117,7 +4230,7 @@ const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
4117
4230
  const validated = UpdateConnectorInputSchema.parse(input);
4118
4231
  const { connectorId, style, label, labelPosition } = validated;
4119
4232
  return `
4120
- // Update connector: ${connectorId}
4233
+ // Update connector: ${oneLine(connectorId)}
4121
4234
  (function() {
4122
4235
  // CONNECTORS LIVE IN THE DIAGRAM SYSTEM, NOT THE ITEM REGISTRY (gate D1,
4123
4236
  // 1.6.19): getItemById never resolves the id connect returned.
@@ -4169,7 +4282,7 @@ const shapes = category ? _lib.getByCategory(category) : _lib.getAll();
4169
4282
  const validated = RemoveConnectorInputSchema.parse(input);
4170
4283
  const { connectorId } = validated;
4171
4284
  return `
4172
- // Remove connector: ${connectorId}
4285
+ // Remove connector: ${oneLine(connectorId)}
4173
4286
  // Resolved through the diagram system (gate D1, 1.6.19), as update_connector is.
4174
4287
  const __cid = ${JSON.stringify(connectorId)};
4175
4288
  const connector = (app.diagramSystem && app.diagramSystem.getConnector(__cid)) || app.getItemById(__cid);
@@ -4178,7 +4291,7 @@ if (!connector) throw new Error('Connector not found: ' + __cid);
4178
4291
  app.diagramSystem.connectorManager.removeConnector(connector);
4179
4292
  app.historyManager.saveState();
4180
4293
 
4181
- ({ connectorId: '${connectorId}', removed: true });
4294
+ ({ connectorId: ${jsLiteral(connectorId)}, removed: true });
4182
4295
  `.trim();
4183
4296
  }
4184
4297
  /**
@@ -4207,17 +4320,17 @@ const isActive = app.diagramSystem.toggle();
4207
4320
  ({ action: 'toggle', active: isActive });
4208
4321
  `.trim();
4209
4322
  case 'setMode':
4210
- const modeStr = mode ? `'${mode}'` : "'select'";
4211
- const shapeStr = shapeType ? `, '${shapeType}'` : '';
4323
+ const modeStr = mode ? jsLiteral(mode) : "'select'";
4324
+ const shapeStr = shapeType ? `, ${jsLiteral(shapeType)}` : '';
4212
4325
  return `
4213
4326
  // Set diagram tool mode
4214
4327
  app.diagramSystem.setMode(${modeStr}${shapeStr});
4215
- ({ action: 'setMode', mode: ${modeStr}${shapeType ? `, shapeType: '${shapeType}'` : ''} });
4328
+ ({ action: 'setMode', mode: ${modeStr}${shapeType ? `, shapeType: ${jsLiteral(shapeType)}` : ''} });
4216
4329
  `.trim();
4217
4330
  default:
4218
4331
  return `
4219
4332
  // Unknown diagram mode action
4220
- throw new Error('Unknown diagram mode action: ${action}');
4333
+ throw new Error('Unknown diagram mode action: ' + ${jsLiteral(action)});
4221
4334
  `.trim();
4222
4335
  }
4223
4336
  }
@@ -4239,7 +4352,7 @@ throw new Error('Unknown diagram mode action: ${action}');
4239
4352
  (async function() {
4240
4353
  const jobOptions = {
4241
4354
  name: ${nameStr},
4242
- screenshotPolicy: '${policyStr}',
4355
+ screenshotPolicy: ${jsLiteral(policyStr)},
4243
4356
  agentMode: true
4244
4357
  };
4245
4358
  `;
@@ -4259,7 +4372,7 @@ throw new Error('Unknown diagram mode action: ${action}');
4259
4372
  if (canvasPreset) {
4260
4373
  code += `
4261
4374
  // Set canvas size to preset (platform name → engine preset key)
4262
- app.setCanvasSize('${canvasPresetFor(canvasPreset)}');
4375
+ app.setCanvasSize(${jsLiteral(canvasPresetFor(canvasPreset))});
4263
4376
  // Allow canvas resize to take effect
4264
4377
  await new Promise(r => setTimeout(r, 50));
4265
4378
  `;
@@ -4303,14 +4416,14 @@ throw new Error('Unknown diagram mode action: ${action}');
4303
4416
  `;
4304
4417
  }
4305
4418
  code += `
4306
- // Return job context with canvas size${wantOntology ? ' + initial ontology' : ''}
4419
+ // Return job context with canvas size${oneLine(wantOntology ? ' + initial ontology' : '')}
4307
4420
  const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
4308
4421
  return {
4309
4422
  success: true,
4310
4423
  jobStarted: true,
4311
4424
  name: ${nameStr},
4312
- screenshotPolicy: '${policyStr}',
4313
- canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'},
4425
+ screenshotPolicy: ${jsLiteral(policyStr)},
4426
+ canvasPreset: ${canvasPreset ? jsLiteral(canvasPreset) : 'null'},
4314
4427
  canvasCleared: ${shouldClear},
4315
4428
  canvasSize: { width: _cs.width || 800, height: _cs.height || 600 }${canvasPreset ? '' : `,
4316
4429
  ...(_boundedFrom ? { note: 'the canvas was unbounded (it exported the artwork, not the board); it is now fixed at ' + (_cs.width || 800) + 'x' + (_cs.height || 600) + ', so stills and video both export that size.' } : {})`}${wantOntology ? `,
@@ -4510,7 +4623,7 @@ throw new Error('Unknown diagram mode action: ${action}');
4510
4623
  if (canvasPreset) {
4511
4624
  code += `
4512
4625
  // Set canvas to preset (platform name → engine preset key)
4513
- app.setCanvasSize('${canvasPresetFor(canvasPreset)}');
4626
+ app.setCanvasSize(${jsLiteral(canvasPresetFor(canvasPreset))});
4514
4627
  // Allow canvas resize to take effect
4515
4628
  await new Promise(r => setTimeout(r, 50));
4516
4629
  `;
@@ -4518,14 +4631,14 @@ throw new Error('Unknown diagram mode action: ${action}');
4518
4631
  if (backgroundColor && !preserveBackground) {
4519
4632
  code += `
4520
4633
  // Set background color
4521
- app.setBackgroundColor('${backgroundColor}');
4634
+ app.setBackgroundColor(${jsLiteral(backgroundColor)});
4522
4635
  `;
4523
4636
  }
4524
4637
  code += `
4525
4638
  // Save state
4526
4639
  if (app.historyManager) app.historyManager.saveState();
4527
4640
  const _cs = app.getCanvasSize ? app.getCanvasSize() : { width: 800, height: 600 };
4528
- return { success: true, reset: true, canvasPreset: ${canvasPreset ? `'${canvasPreset}'` : 'null'}, canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
4641
+ return { success: true, reset: true, canvasPreset: ${canvasPreset ? jsLiteral(canvasPreset) : 'null'}, canvasSize: { width: _cs.width || 800, height: _cs.height || 600 } };
4529
4642
  })();
4530
4643
  `;
4531
4644
  return code.trim();
@@ -4546,7 +4659,7 @@ throw new Error('Unknown diagram mode action: ${action}');
4546
4659
  // anything runs; a direct caller gets the op's own failure instead.
4547
4660
  const timelines = compileBatchTimelines(operations);
4548
4661
  let code = `
4549
- // Batch execute ${operations.length} operations
4662
+ // Batch execute ${oneLine(operations.length)} operations
4550
4663
  (async function() {
4551
4664
  // An op's own verdict. Deliberately narrow, matching the server-side rule:
4552
4665
  // only an explicit success:false or ok:false is a failure, because most ops
@@ -4619,7 +4732,7 @@ throw new Error('Unknown diagram mode action: ${action}');
4619
4732
  const opItemJs = typeof opItem === 'string' && /^\$\d+$/.test(opItem) ? `__refName(${opItem.slice(1)})` : JSON.stringify(opItem);
4620
4733
  const opStep = typeof o._step === 'string' ? `op ${index}: ${o._step}` : `op ${index} (${op.type})`;
4621
4734
  code += `
4622
- // Operation ${index}: ${op.type}
4735
+ // Operation ${oneLine(index)}: ${oneLine(op.type)}
4623
4736
  try {
4624
4737
  const result${index} = await (async () => {
4625
4738
  ${opCode}
@@ -4803,14 +4916,55 @@ return { itemId: targetId, animationType: ${JSON.stringify(String(op.animationTy
4803
4916
  return `
4804
4917
  throw new Error(${JSON.stringify(String(op.relationType) + ': ' + fireIf)});
4805
4918
  `;
4919
+ // No targetId is a relation on the item alone (the engine takes a null target, as staggered_with does); a
4920
+ // custom relation whose compute ignores the target needed a stand-in before.
4921
+ const noTarget = op.targetId === undefined || op.targetId === null || op.targetId === '';
4922
+ // A NAME PATTERN AND AN UPDATE (mcp-server#37). sourceId "dot-*" binds every item named like it, in name
4923
+ // order (dot-2 before dot-10), each with index and count added to its params, so "12 → 20 dots" is
4924
+ // adding members. update: true changes the params of the relation already bound instead of adding one.
4925
+ const glob = typeof op.sourceId === 'string' && op.sourceId.includes('*') && !op.sourceId.startsWith('$');
4926
+ if (glob || op.update === true) {
4927
+ const type = jsLiteral(String(op.relationType));
4928
+ const members = glob
4929
+ ? `const __re = new RegExp(${jsLiteral('^' + String(op.sourceId).split('*').map((p) => p.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('.*') + '$')});
4930
+ const __members = app.itemRegistry.getAll()
4931
+ .map((e) => ({ id: e.id || e.itemId, name: (e.item && ((e.item.data && e.item.data.name) || e.item.name)) || '' }))
4932
+ .filter((m) => __re.test(m.name))
4933
+ .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
4934
+ if (!__members.length) throw new Error('no item is named like ' + ${jsLiteral(op.sourceId)} + ': name items with properties.name');`
4935
+ : `const __members = [{ id: ${srcRef} }];
4936
+ if (!app.getItemById(__members[0].id)) throw new Error('Source not found: ' + __members[0].id);`;
4937
+ return `
4938
+ ${members}
4939
+ const targetId = ${noTarget ? 'null' : tgtRef};
4940
+ const target = ${noTarget ? 'null' : 'app.getItemById(targetId)'};
4941
+ ${noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n"}${op.update === true ? "if (typeof app.modifyRelationParams !== 'function') throw new Error('this studio cannot change a bound relation (app.modifyRelationParams): update PinePaper Studio');\n" : ''}const __done = [], __failed = [];
4942
+ __members.forEach((m, i) => {
4943
+ const __p = Object.assign({}, ${relOpts}${glob ? ', { index: i, count: __members.length }' : ''});
4944
+ const ok = ${op.update === true
4945
+ ? `app.modifyRelationParams(m.id, targetId, ${type}, __p) || (targetId === null && app.modifyRelationParams(m.id, undefined, ${type}, __p))`
4946
+ : glob
4947
+ // Already bound (a re-run after adding members): its params change, so index and count follow the new set.
4948
+ ? `((app.relationRegistry && app.relationRegistry.getAssociations(m.id, ${type}).some((a) => a.target === targetId || (targetId === null && a.target == null)) && typeof app.modifyRelationParams === 'function')
4949
+ ? (app.modifyRelationParams(m.id, targetId, ${type}, __p) || (targetId === null && app.modifyRelationParams(m.id, undefined, ${type}, __p)))
4950
+ : app.addRelation(app.getItemById(m.id), target, ${type}, __p))`
4951
+ : `app.addRelation(app.getItemById(m.id), target, ${type}, __p)`};
4952
+ (ok ? __done : __failed).push(m.id);
4953
+ });
4954
+ if (!__done.length) {
4955
+ const __why = ${op.update === true ? `'no ' + ${type} + ' relation is bound from ' + __failed.join(', ') + (targetId === null ? '' : ' to ' + targetId) + ' to change: bind it first (a relation op without update)'` : relationRefusalReasonJs(type)};
4956
+ return { success: false, relationType: ${type}, sourceIds: __failed, error: ${op.update === true ? '__why' : `'the studio refused the ' + ${type} + ' relation for ' + __failed.join(', ') + ' — nothing was bound' + (__why ? ': ' + __why : '.')`} };
4957
+ }
4958
+ return { success: true, relationType: ${type}, ${op.update === true ? 'updated' : 'bound'}: __done, targetId${glob ? ', count: __members.length' : ''}${(() => { const w = relationOptionWarning(op.relationType, op.relationOptions, ['index', 'count']); return w ? `, optionWarning: ${jsLiteral(w)}` : ''; })()}, ...(__failed.length ? { failed: __failed, warning: __failed.length + ' of ' + __members.length + ' were not ${op.update === true ? 'changed' : 'bound'}: ' + __failed.join(', ') } : {}) };
4959
+ `;
4960
+ }
4806
4961
  return `
4807
4962
  const sourceId = ${srcRef};
4808
- const targetId = ${tgtRef};
4963
+ const targetId = ${noTarget ? 'null' : tgtRef};
4809
4964
  const source = app.getItemById(sourceId);
4810
- const target = app.getItemById(targetId);
4965
+ const target = ${noTarget ? 'null' : 'app.getItemById(targetId)'};
4811
4966
  if (!source) throw new Error('Source not found: ' + sourceId);
4812
- if (!target) throw new Error('Target not found: ' + targetId);
4813
- const relationId = app.addRelation(source, target, ${JSON.stringify(String(op.relationType))}, ${relOpts});
4967
+ ${noTarget ? '' : "if (!target) throw new Error('Target not found: ' + targetId);\n"}const relationId = app.addRelation(source, target, ${JSON.stringify(String(op.relationType))}, ${relOpts});
4814
4968
  // NOT BOUND IS NOT A SUCCESS (owner rule 1, B119). addRelation answers false
4815
4969
  // when it refuses — spring_follow "not loaded yet" — and this op returned
4816
4970
  // { relationId: false } with nothing marking it failed, so the batch said
@@ -4820,7 +4974,7 @@ if (!relationId) {
4820
4974
  return { success: false, relationId: false, sourceId, targetId, relationType: ${JSON.stringify(op.relationType)},
4821
4975
  error: 'the studio refused the ' + ${JSON.stringify(op.relationType)} + ' relation between ' + sourceId + ' and ' + targetId + ' — nothing was bound' + (__why ? ': ' + __why : '.') };
4822
4976
  }
4823
- return { relationId, sourceId, targetId, relationType: ${JSON.stringify(String(op.relationType))} };
4977
+ return { relationId, sourceId, targetId, relationType: ${JSON.stringify(String(op.relationType))}${(() => { const w = relationOptionWarning(op.relationType, op.relationOptions); return w ? `, warning: ${jsLiteral(w)}` : ''; })()} };
4824
4978
  `;
4825
4979
  case 'delete':
4826
4980
  const delRef = batchRef(op.itemId);
@@ -4876,7 +5030,7 @@ return { success: true, width: (_cs && _cs.width) || ${w}, height: (_cs && _cs.h
4876
5030
  const foldTarget = mergePlan?.foldedInto.get(index);
4877
5031
  if (foldTarget !== undefined) {
4878
5032
  return `
4879
- // Keyframes merged into operation ${foldTarget} (same target). A keyframe track
5033
+ // Keyframes merged into operation ${oneLine(foldTarget)} (same target). A keyframe track
4880
5034
  // is replaced wholesale, so emitting both here would silently drop one.
4881
5035
  return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget} };
4882
5036
  `;
@@ -4906,7 +5060,7 @@ return { itemId: ${batchRef(op.itemId)}, merged: true, mergedInto: ${foldTarget}
4906
5060
  }
4907
5061
  const kfUnits = op.timeUnits;
4908
5062
  if (kfUnits === 'seconds' || kfUnits === 'ms')
4909
- kfExtra.push(`timeUnits: '${kfUnits}'`);
5063
+ kfExtra.push(`timeUnits: ${jsLiteral(kfUnits)}`);
4910
5064
  const kfExtraStr = kfExtra.length > 0 ? `, ${kfExtra.join(', ')}` : '';
4911
5065
  const kfTextKeysJs = unanimatedTextKeysJs((kfSource || []).flatMap((k) => Object.keys(k.properties ?? {})));
4912
5066
  const kfTextCheck = kfTextKeysJs ? `\nconst _kfIgnored = ${kfTextKeysJs};` : '';
@@ -4933,12 +5087,12 @@ const targetId = ${maskItemRef};
4933
5087
  const item = app.getItemById(targetId);
4934
5088
  if (!item) throw new Error('Item not found: ' + targetId);
4935
5089
  if (!app.applyAnimatedMask) throw new Error('Mask system not available');
4936
- const maskedGroup = app.applyAnimatedMask(item, '${preset}', ${maskOpts});
5090
+ const maskedGroup = app.applyAnimatedMask(item, ${jsLiteral(preset)}, ${maskOpts});
4937
5091
  const maskRej = maskedGroup && maskedGroup.data && maskedGroup.data.maskAnimationRejected;
4938
5092
  if (maskRej) {
4939
5093
  return { success: false, itemId: targetId, error: 'Masked, but animation preset "' + maskRej.requested + '" is unknown — it will NOT animate.', requested: maskRej.requested, known: maskRej.known };
4940
5094
  }
4941
- return { success: !!maskedGroup, itemId: targetId, preset: '${preset}' };
5095
+ return { success: !!maskedGroup, itemId: targetId, preset: ${jsLiteral(preset)} };
4942
5096
  `;
4943
5097
  }
4944
5098
  case 'apply_effect': {
@@ -4952,12 +5106,12 @@ if (!item) throw new Error('Item not found: ' + targetId);
4952
5106
  // shape, so an effect the engine refused — an unknown preset, a facade that
4953
5107
  // is not loaded — was recorded as applied. The batch cannot report what the
4954
5108
  // op never tells it.
4955
- const _eff = app.applyEffect(item, '${op.effectType || 'sparkle'}', ${effParams});
5109
+ const _eff = app.applyEffect(item, ${jsLiteral(op.effectType || 'sparkle')}, ${effParams});
4956
5110
  if (_eff && (_eff.success === false || _eff.ok === false)) {
4957
- return { success: false, itemId: targetId, effectType: '${op.effectType || 'sparkle'}',
5111
+ return { success: false, itemId: targetId, effectType: ${jsLiteral(op.effectType || 'sparkle')},
4958
5112
  error: _eff.error || _eff.reason || 'the effect was refused without a reason' };
4959
5113
  }
4960
- return { itemId: targetId, effectType: '${op.effectType || 'sparkle'}' };
5114
+ return { itemId: targetId, effectType: ${jsLiteral(op.effectType || 'sparkle')} };
4961
5115
  `;
4962
5116
  }
4963
5117
  case 'play_timeline': {
@@ -5005,7 +5159,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
5005
5159
  if (typeof length === 'number')
5006
5160
  range.length = length;
5007
5161
  return `
5008
- // Read export chunk at ${offset}
5162
+ // Read export chunk at ${oneLine(offset)}
5009
5163
  (async function() {
5010
5164
  if (!app.exportEngine || typeof app.exportEngine.readExport !== 'function') {
5011
5165
  return { ok: false, reason: 'app.exportEngine.readExport unavailable — update FxTool' };
@@ -5050,9 +5204,13 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
5050
5204
  }
5051
5205
  generateAgentExport(input) {
5052
5206
  const validated = AgentExportInputSchema.parse(input);
5053
- const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames } = validated;
5207
+ const { platform, format, quality, framing, duration, estimateOnly, scale, fps, pdf: pdfOpts, region, time: stillTime, maxBytes, loop: gifLoop, broadcast, broadcastHeadroom, bitrate, minBitrate, bitrateMode, transparent, alphaQuantizer, deterministic, motionBlur, ad: adOpts, frames, startTime } = validated;
5054
5208
  // Encoder options (FxTool 16719759); the schema keeps them to mp4 / webm.
5055
5209
  const videoEncodeOpts = {};
5210
+ // A RANGE of the timeline (FxTool #250): frames for t in [startTime, startTime + duration), each exactly the frame
5211
+ // a full export has at that time (the engine renders the frames before startTime without encoding them).
5212
+ if (startTime !== undefined)
5213
+ videoEncodeOpts.startTime = startTime;
5056
5214
  if (broadcast)
5057
5215
  videoEncodeOpts.broadcast = true;
5058
5216
  // Headroom defaults to 12 with broadcast (retest of caa9560): on a
@@ -5172,10 +5330,10 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
5172
5330
  if (exportFormat === 'srt' || exportFormat === 'vtt' || exportFormat === 'scc') {
5173
5331
  const fmt = exportFormat;
5174
5332
  return `
5175
- // Export: captions (${fmt}) from text items with a lifetime
5333
+ // Export: captions (${oneLine(fmt)}) from text items with a lifetime
5176
5334
  (function() {
5177
5335
  if (!app.itemRegistry || typeof app.itemRegistry.getAll !== 'function') {
5178
- return { success: false, format: '${fmt}', error: 'this studio cannot list items (app.itemRegistry.getAll) — update PinePaper Studio.' };
5336
+ return { success: false, format: ${jsLiteral(fmt)}, error: 'this studio cannot list items (app.itemRegistry.getAll) — update PinePaper Studio.' };
5179
5337
  }
5180
5338
  const texts = app.itemRegistry.getAll().filter(function(e) {
5181
5339
  const it = e && e.item;
@@ -5183,7 +5341,7 @@ return { success: true, action: 'seek', time: ${op.time || 0} };
5183
5341
  });
5184
5342
  const timed = texts.filter(function(e) { return e.item.data && typeof e.item.data.bornAt === 'number'; });
5185
5343
  if (!timed.length) {
5186
- return { success: false, format: '${fmt}', error: 'no captions to export: no text item has a lifetime. Stage each caption with properties.bornAt / ttl (seconds) on create_item or modify_item, then export ${fmt}.', untimedText: texts.length };
5344
+ return { success: false, format: ${jsLiteral(fmt)}, error: 'no captions to export: no text item has a lifetime. Stage each caption with properties.bornAt / ttl (seconds) on create_item or modify_item, then export ${fmt}.', untimedText: texts.length };
5187
5345
  }
5188
5346
  const end = ${videoDuration};
5189
5347
  const ts = function(t, sep) {
@@ -5203,7 +5361,7 @@ ${fmt === 'scc' ? ` return { success: true, format: 'scc', sccCues: cues, cues:
5203
5361
  return ${fmt === 'srt' ? `(i + 1) + '\\n' + ts(c.start, ',') + ' --> ' + ts(c.stop, ',')` : `ts(c.start, '.') + ' --> ' + ts(c.stop, '.')`} + '\\n' + c.text;
5204
5362
  }).join('\\n\\n') + '\\n';
5205
5363
  const text = ${fmt === 'vtt' ? `'WEBVTT\\n\\n' + body` : 'body'};
5206
- return { success: true, format: '${fmt}', data: text, mimeType: '${fmt === 'srt' ? 'application/x-subrip' : 'text/vtt'}', size: text.length, cues: cues.length, untimedText: texts.length - timed.length };
5364
+ return { success: true, format: ${jsLiteral(fmt)}, data: text, mimeType: ${jsLiteral(fmt === 'srt' ? 'application/x-subrip' : 'text/vtt')}, size: text.length, cues: cues.length, untimedText: texts.length - timed.length };
5207
5365
  })();`.trim();
5208
5366
  }
5209
5367
  if (exportFormat === 'wav') {
@@ -5253,7 +5411,7 @@ ${fmt === 'scc' ? ` return { success: true, format: 'scc', sccCues: cues, cues:
5253
5411
  bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
5254
5412
  }
5255
5413
  return {
5256
- success: true, format: 'wav', platform: '${platform}',
5414
+ success: true, format: 'wav', platform: ${jsLiteral(platform)},
5257
5415
  data: 'data:audio/wav;base64,' + btoa(bin),
5258
5416
  size: bytes.length,
5259
5417
  duration: r.duration, sampleRate: r.sampleRate, sounds: r.sounds, placed: r.placed,
@@ -5264,12 +5422,14 @@ ${fmt === 'scc' ? ` return { success: true, format: 'scc', sccCues: cues, cues:
5264
5422
  })();`.trim();
5265
5423
  }
5266
5424
  return `
5267
- // Smart export for ${platform}
5425
+ // Smart export for ${oneLine(platform)}
5268
5426
  (async function() {${ENSURE_EXPORT_ENGINE}
5269
- const platform = '${platform}';
5270
- const format = '${exportFormat}';
5271
- const quality = '${qualityLevel}';
5272
- const framing = '${framing}';
5427
+ const platform = ${jsLiteral(platform)};
5428
+ const format = ${jsLiteral(exportFormat)};${startTime !== undefined ? `
5429
+ if (format !== 'mp4' && format !== 'webm') return { success: false, format, error: 'startTime exports a range of the timeline, which only mp4 and webm can: ' + format + ' would start from 0.' };
5430
+ if (!(app.exportEngine && app.exportEngine.supportsTimeRange === true)) return { success: false, format, error: 'this studio cannot export a range of the timeline (startTime): update PinePaper Studio, or export from 0.' };` : ''}
5431
+ const quality = ${jsLiteral(qualityLevel)};
5432
+ const framing = ${jsLiteral(framing)};
5273
5433
  // An explicit fps wins over the tier's. The tiers bundle frame rate with
5274
5434
  // compression (draft 15 / standard 30 / high 60), so "higher quality" also
5275
5435
  // doubled the frame count and the render time — a surprise the schema used
@@ -6438,7 +6598,7 @@ ${(() => {
6438
6598
  if (__bs.stale) {
6439
6599
  result.stale = true;
6440
6600
  result.fidelity = result.fidelity || { warnings: [] };
6441
- result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'stale_build', message: 'this studio is running an old build (' + __bs.buildId + '; the live build is ' + __bs.liveBuildId + '): this file was made by the old code. Start a new job (pinepaper_agent_start_job, which reloads it) and export again.' }]);
6601
+ result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'stale_build', message: 'this studio is running an old build (' + __bs.buildId + '; the live build is ' + __bs.liveBuildId + '): this file was made by the old code, so it may differ from what the live build makes. pinepaper_clear_canvas (or pinepaper_agent_start_job) loads the live build; it empties the canvas, so rebuild the scene and export again if the difference matters.' }]);
6442
6602
  }
6443
6603
  }
6444
6604
  } catch (_) { /* older studio */ }
@@ -6450,7 +6610,7 @@ ${(() => {
6450
6610
  if (__rl && __rl.enabled) {
6451
6611
  result.fidelity = result.fidelity || { warnings: [] };
6452
6612
  if (['png', 'jpg', 'webp', 'pdf', 'mp4', 'webm'].indexOf(${JSON.stringify(format)}) === -1) {
6453
- result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ${format} is not relit. Export png, pdf, mp4 or webm to keep the lighting.' }]);
6613
+ result.fidelity.warnings = (result.fidelity.warnings || []).concat([{ code: 'relight_not_applied', message: 'a relight rig is set, but ' + ${jsLiteral(format)} + ' is not relit. Export png, pdf, mp4 or webm to keep the lighting.' }]);
6454
6614
  } else {
6455
6615
  result.relight = { applied: true, lights: (__rl.lights || []).length };
6456
6616
  }
@@ -6681,7 +6841,7 @@ ${(() => {
6681
6841
  const validated = AnimateLetterCollageInputSchema.parse(input);
6682
6842
  const { collageId, animationType, staggerDelay = 0.1, animationSpeed = 1, } = validated;
6683
6843
  return `
6684
- // Animate letter collage: ${collageId}
6844
+ // Animate letter collage: ${oneLine(collageId)}
6685
6845
  (function() {
6686
6846
  const collageId = ${JSON.stringify(collageId)};
6687
6847
  const animationType = ${JSON.stringify(animationType)};
@@ -6778,10 +6938,10 @@ ${(() => {
6778
6938
  }
6779
6939
 
6780
6940
  try {
6781
- const result = await app.mapSystem.loadMap('${mapId}', ${optionsStr});
6941
+ const result = await app.mapSystem.loadMap(${jsLiteral(mapId)}, ${optionsStr});
6782
6942
  return {
6783
6943
  success: true,
6784
- mapId: result.mapId || '${mapId}',
6944
+ mapId: result.mapId || ${jsLiteral(mapId)},
6785
6945
  // regionPaths is a Map: .length was undefined, so every load reported
6786
6946
  // regions: 0 (gate D3). size, with the feature count as a fallback.
6787
6947
  regions: (result.regions && (result.regions.size ?? result.regions.length)) || (result.geoData && result.geoData.features ? result.geoData.features.length : 0),
@@ -7198,7 +7358,7 @@ ${(() => {
7198
7358
  }
7199
7359
 
7200
7360
  try {
7201
- const result = await app.mapSystem.importCustomMap('${validated.url}', ${optionsStr});
7361
+ const result = await app.mapSystem.importCustomMap(${jsLiteral(validated.url)}, ${optionsStr});
7202
7362
  // A summary, not the engine's return: that carries the Paper group and
7203
7363
  // the region Map, which serialised into a raw Paper.js dump (gate N1).
7204
7364
  const __r = result && result.regions;
@@ -7316,7 +7476,7 @@ ${(() => {
7316
7476
  loop: ${validated.loop !== false},
7317
7477
  colors: ${colorsStr},${validated.waveSpeed !== undefined ? `
7318
7478
  waveSpeed: ${validated.waveSpeed},` : ''}
7319
- waveDirection: '${validated.waveDirection || 'horizontal'}'
7479
+ waveDirection: ${jsLiteral(validated.waveDirection || 'horizontal')}
7320
7480
  });
7321
7481
  return { success: true, animatedRegions: result?.animatedRegions || [], totalRegions: result?.totalRegions || 0 };
7322
7482
  } catch (error) {
@@ -7388,7 +7548,7 @@ ${(() => {
7388
7548
  includeSelected: ${validated.includeSelected !== false},
7389
7549
  includeColors: ${validated.includeColors !== false},
7390
7550
  download: ${validated.download === true},
7391
- filename: '${validated.filename || 'map-regions.csv'}'
7551
+ filename: ${jsLiteral(validated.filename || 'map-regions.csv')}
7392
7552
  });
7393
7553
  return { success: true, csv: result?.csv || '', regionCount: result?.regionCount || 0 };
7394
7554
  } catch (error) {
@@ -7645,7 +7805,7 @@ ${(() => {
7645
7805
 
7646
7806
  const item = (app.getItemById && app.getItemById(${JSON.stringify(itemId)})) || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(${JSON.stringify(itemId)}) && (app.itemRegistry.get(${JSON.stringify(itemId)}).item || app.itemRegistry.get(${JSON.stringify(itemId)})));
7647
7807
  if (!item) {
7648
- return { success: false, error: 'Item ${itemId} not found' };
7808
+ return { success: false, error: 'Item ' + ${jsLiteral(itemId)} + ' not found' };
7649
7809
  }
7650
7810
 
7651
7811
  try {
@@ -7674,7 +7834,7 @@ ${(() => {
7674
7834
 
7675
7835
  const item = (app.getItemById && app.getItemById(${JSON.stringify(itemId)})) || (app.itemRegistry && app.itemRegistry.get && app.itemRegistry.get(${JSON.stringify(itemId)}) && (app.itemRegistry.get(${JSON.stringify(itemId)}).item || app.itemRegistry.get(${JSON.stringify(itemId)})));
7676
7836
  if (!item) {
7677
- return { success: false, error: 'Item ${itemId} not found' };
7837
+ return { success: false, error: 'Item ' + ${jsLiteral(itemId)} + ' not found' };
7678
7838
  }
7679
7839
 
7680
7840
  try {
@@ -8047,10 +8207,10 @@ ${(() => {
8047
8207
  const paramsStr = validated.params ? JSON.stringify(validated.params) : '{}';
8048
8208
  const templatesStr = validated.templates ? JSON.stringify(validated.templates) : '[]';
8049
8209
  return `
8050
- // Register custom relation: ${validated.name}
8210
+ // Register custom relation: ${oneLine(validated.name)}
8051
8211
  (function() {
8052
8212
  try {
8053
- app.registerRelationRule('${validated.name}', {
8213
+ app.registerRelationRule(${jsLiteral(validated.name)}, {
8054
8214
  description: ${JSON.stringify(validated.description || '')},
8055
8215
  params: ${paramsStr},
8056
8216
  compute: (ctx) => {
@@ -8064,7 +8224,11 @@ ${(() => {
8064
8224
  continuous: ${validated.continuous !== false},
8065
8225
  priority: ${validated.priority || 0}
8066
8226
  });
8067
- return { success: true, relationName: '${validated.name}', message: 'Custom relation registered successfully' };
8227
+ // The definition as given, kept on the page so a later call can read it back (pinepaper_lint_scene
8228
+ // customRelations): the engine keeps the compiled rule, not the text an agent would edit.
8229
+ window.__ppCustomRelations = window.__ppCustomRelations || {};
8230
+ window.__ppCustomRelations[${jsLiteral(validated.name)}] = ${jsLiteral({ name: validated.name, ...(validated.description ? { description: validated.description } : {}), params: validated.params ?? {}, computeFunction: validated.computeFunction, applyFunction: validated.applyFunction })};
8231
+ return { success: true, relationName: ${jsLiteral(validated.name)}, message: 'Custom relation registered successfully' };
8068
8232
  } catch (error) {
8069
8233
  return { success: false, error: error.message };
8070
8234
  }
@@ -8095,7 +8259,7 @@ ${(() => {
8095
8259
  // __ppValue with the scene lint beside it, and a throw comes back as { step, itemId, reason }, never a bare
8096
8260
  // message (and never "undefined" for a thrown non-Error).
8097
8261
  return `
8098
- // ${description}
8262
+ // ${oneLine(description)}
8099
8263
  (async function() {
8100
8264
  // THE SCENE LINT AFTER THE CODE (FxTool lintScene().warnings): what the scene will show wrong without anything
8101
8265
  // failing (a centred title that runs out of the frame). Carried out under __ppLint; the server moves it beside the
@@ -8151,21 +8315,21 @@ ${(() => {
8151
8315
  // Per-preset options. Custom keyframes are an animated-mask extension —
8152
8316
  // when supplied, fall through to maskingSystem.applyCustomMask instead.
8153
8317
  return `
8154
- // Apply animated mask to ${validated.itemId}
8318
+ // Apply animated mask to ${oneLine(validated.itemId)}
8155
8319
  (function() {
8156
8320
  if (!app.applyAnimatedMask) {
8157
8321
  return { success: false, error: 'Mask system not available' };
8158
8322
  }
8159
- const item = app.getItemById('${validated.itemId}');
8160
- if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
8323
+ const item = app.getItemById(${jsLiteral(validated.itemId)});
8324
+ if (!item) return { success: false, error: 'Item not found: ' + ${jsLiteral(validated.itemId)} };
8161
8325
 
8162
8326
  try {
8163
8327
  const mergedOpts = Object.assign({}, ${optionsStr}, ${maskOptionsStr});
8164
8328
  let maskedGroup;
8165
8329
  if (${keyframesStr} && app.maskingSystem && app.maskingSystem.applyCustomMask) {
8166
- maskedGroup = app.maskingSystem.applyCustomMask(item, '${customShape}', ${keyframesStr}, mergedOpts);
8330
+ maskedGroup = app.maskingSystem.applyCustomMask(item, ${jsLiteral(customShape)}, ${keyframesStr}, mergedOpts);
8167
8331
  } else {
8168
- maskedGroup = app.applyAnimatedMask(item, '${preset}', mergedOpts);
8332
+ maskedGroup = app.applyAnimatedMask(item, ${jsLiteral(preset)}, mergedOpts);
8169
8333
  }
8170
8334
  // A mask with an unknown ANIMATION preset still masks — the item just never
8171
8335
  // animates — so !!maskedGroup is true either way. The engine records which.
@@ -8173,13 +8337,13 @@ ${(() => {
8173
8337
  if (rej) {
8174
8338
  return {
8175
8339
  success: false,
8176
- itemId: '${validated.itemId}',
8340
+ itemId: ${jsLiteral(validated.itemId)},
8177
8341
  error: 'Masked, but the animation preset "' + rej.requested + '" is unknown — it will NOT animate.',
8178
8342
  requested: rej.requested,
8179
8343
  known: rej.known,
8180
8344
  };
8181
8345
  }
8182
- return { success: !!maskedGroup, itemId: '${validated.itemId}', preset: '${preset}' };
8346
+ return { success: !!maskedGroup, itemId: ${jsLiteral(validated.itemId)}, preset: ${jsLiteral(preset)} };
8183
8347
  } catch (error) {
8184
8348
  return { success: false, error: error.message };
8185
8349
  }
@@ -8191,17 +8355,17 @@ ${(() => {
8191
8355
  const keyframesStr = JSON.stringify(validated.keyframes);
8192
8356
  const maskOptionsStr = validated.maskOptions ? JSON.stringify(validated.maskOptions) : '{}';
8193
8357
  return `
8194
- // Apply custom mask to ${validated.itemId}
8358
+ // Apply custom mask to ${oneLine(validated.itemId)}
8195
8359
  (function() {
8196
8360
  if (!app.maskingSystem || !app.maskingSystem.applyCustomMask) {
8197
8361
  return { success: false, error: 'Mask system not available' };
8198
8362
  }
8199
- const item = app.getItemById('${validated.itemId}');
8200
- if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
8363
+ const item = app.getItemById(${jsLiteral(validated.itemId)});
8364
+ if (!item) return { success: false, error: 'Item not found: ' + ${jsLiteral(validated.itemId)} };
8201
8365
 
8202
8366
  try {
8203
- const maskedGroup = app.maskingSystem.applyCustomMask(item, '${validated.maskType}', ${keyframesStr}, ${maskOptionsStr});
8204
- return { success: !!maskedGroup, itemId: '${validated.itemId}', maskType: '${validated.maskType}' };
8367
+ const maskedGroup = app.maskingSystem.applyCustomMask(item, ${jsLiteral(validated.maskType)}, ${keyframesStr}, ${maskOptionsStr});
8368
+ return { success: !!maskedGroup, itemId: ${jsLiteral(validated.itemId)}, maskType: ${jsLiteral(validated.maskType)} };
8205
8369
  } catch (error) {
8206
8370
  return { success: false, error: error.message };
8207
8371
  }
@@ -8211,20 +8375,20 @@ ${(() => {
8211
8375
  generateRemoveMask(input) {
8212
8376
  const validated = RemoveMaskInputSchema.parse(input);
8213
8377
  return `
8214
- // Remove mask from ${validated.itemId}
8378
+ // Remove mask from ${oneLine(validated.itemId)}
8215
8379
  (function() {
8216
8380
  if (!app.removeMask) {
8217
8381
  return { success: false, error: 'Mask system not available' };
8218
8382
  }
8219
8383
  // The item id may point at either the masked group OR the original item;
8220
8384
  // app.removeMask expects the masked group, so resolve and walk up if needed.
8221
- const item = app.getItemById('${validated.itemId}');
8222
- if (!item) return { success: false, error: 'Item not found: ${validated.itemId}' };
8385
+ const item = app.getItemById(${jsLiteral(validated.itemId)});
8386
+ if (!item) return { success: false, error: 'Item not found: ' + ${jsLiteral(validated.itemId)} };
8223
8387
  const maskedGroup = (item.data && item.data.isMaskedGroup) ? item : (item.parent && item.parent.data && item.parent.data.isMaskedGroup ? item.parent : item);
8224
8388
 
8225
8389
  try {
8226
8390
  app.removeMask(maskedGroup);
8227
- return { success: true, itemId: '${validated.itemId}', message: 'Mask removed' };
8391
+ return { success: true, itemId: ${jsLiteral(validated.itemId)}, message: 'Mask removed' };
8228
8392
  } catch (error) {
8229
8393
  return { success: false, error: error.message };
8230
8394
  }
@@ -8253,7 +8417,7 @@ ${(() => {
8253
8417
  return { success: false, error: 'app.listAnimatableProperties unavailable — update the studio. The keyframe tool lists the properties it accepts in its own description meanwhile.' };
8254
8418
  }
8255
8419
  const item = app.getItemById(${JSON.stringify(itemId)});
8256
- if (!item) { return { success: false, error: 'no item ${itemId}' }; }
8420
+ if (!item) { return { success: false, error: 'no item ' + ${jsLiteral(itemId)} }; }
8257
8421
  const r = app.listAnimatableProperties(item);
8258
8422
  return { success: true, ...r };
8259
8423
  })();`.trim();
@@ -8351,7 +8515,7 @@ ${(() => {
8351
8515
  generateApplyTemplate(input) {
8352
8516
  const { templateId, category, listOnly } = input;
8353
8517
  if (listOnly || !templateId) {
8354
- const categoryFilter = category ? `'${category}'` : 'null';
8518
+ const categoryFilter = category ? jsLiteral(category) : 'null';
8355
8519
  return `
8356
8520
  // List available templates
8357
8521
  (async function() {
@@ -8379,7 +8543,7 @@ ${(() => {
8379
8543
  `.trim();
8380
8544
  }
8381
8545
  return `
8382
- // Apply template: ${templateId}
8546
+ // Apply template: ${oneLine(templateId)}
8383
8547
  (async function() {
8384
8548
  if (!app.templateManager) {
8385
8549
  return { error: 'Template manager not available. Make sure PinePaper Studio is loaded.' };
@@ -8426,7 +8590,7 @@ ${(() => {
8426
8590
  let fidelity = {};
8427
8591
  try {
8428
8592
  const tpl = (app.templateManager.getAllTemplates() || [])
8429
- .find(function(t) { return t && (t.id === '${templateId}' || t.templateId === '${templateId}'); });
8593
+ .find(function(t) { return t && (t.id === ${jsLiteral(templateId)} || t.templateId === ${jsLiteral(templateId)}); });
8430
8594
  const items = tpl && tpl.data && Array.isArray(tpl.data.items) ? tpl.data.items : null;
8431
8595
  const declared = items ? items.filter(function(i) { return i && i.type === 'group' && i.paperJSON; }).length : 0;
8432
8596
  if (declared > 0 && app.itemRegistry && typeof app.itemRegistry.getAll === 'function') {
@@ -8447,7 +8611,7 @@ ${(() => {
8447
8611
  }
8448
8612
  } catch (e) { /* a check that cannot run must not fail the load */ }
8449
8613
 
8450
- return { success: true, templateId: '${templateId}', message: 'Template applied successfully. Canvas has been replaced with template content.', ...fidelity };
8614
+ return { success: true, templateId: ${jsLiteral(templateId)}, message: 'Template applied successfully. Canvas has been replaced with template content.', ...fidelity };
8451
8615
  } catch (e) {
8452
8616
  return { error: 'Failed to apply template: ' + e.message };
8453
8617
  }
@@ -8497,7 +8661,7 @@ ${(() => {
8497
8661
  // The bytes then go in as a DATA URL, which img.src loads without touching
8498
8662
  // the network again — so a host that allows fetch but blocks the image
8499
8663
  // load (CORS on <img>, a tainted-canvas refusal) now works too.
8500
- let src = '${url}';
8664
+ let src = ${jsLiteral(url)};
8501
8665
  // A STAGED KEY, not the bytes. The handler writes anything over ~64KB to
8502
8666
  // window.__ppStage and passes the key instead, because the governor's
8503
8667
  // loop-guard transform bails on a code string that large and takes the
@@ -8559,7 +8723,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
8559
8723
  // refused instead of answered with the raster's bounds.
8560
8724
  const masked = app.imageTools.applyMask(raster, ${JSON.stringify(mask)});
8561
8725
  if (!masked) {
8562
- return { success: false, itemId: itemId, error: 'the ${mask} mask was not applied — the image may not be a raster yet. The unmasked image is on the canvas as ' + itemId + '.' };
8726
+ return { success: false, itemId: itemId, error: 'the ' + ${jsLiteral(mask)} + ' mask was not applied — the image may not be a raster yet. The unmasked image is on the canvas as ' + itemId + '.' };
8563
8727
  }
8564
8728
  if (app.itemRegistry && typeof app.itemRegistry.rebind === 'function') app.itemRegistry.rebind(itemId, masked);
8565
8729
  placed = masked;
@@ -8586,7 +8750,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
8586
8750
  success: true,
8587
8751
  itemId: itemId,
8588
8752
  message: 'Image imported and placed on canvas.',${name ? `\n name: ${JSON.stringify(name)},` : ''}
8589
- bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: '${mask}'` : ''}${hasShadow ? ',\n shadow: __shadow' : ''},
8753
+ bounds: { x: b.x, y: b.y, width: b.width, height: b.height }${mask ? `,\n mask: ${jsLiteral(mask)}` : ''}${hasShadow ? ',\n shadow: __shadow' : ''},
8590
8754
  // Stored smaller than its source (the studio's cap): said, with the
8591
8755
  // way to keep every pixel. The studio reports native size where it can.
8592
8756
  ...((entry && entry.nativeWidth && entry.width && entry.nativeWidth > entry.width)
@@ -8680,7 +8844,7 @@ ${mask ? ` // A MASK REPLACES THE RASTER (round 8 DD, 4.10). applyMask builds
8680
8844
  (function() {
8681
8845
  ${guard}
8682
8846
  const scene = app.sceneManager.getScene(${JSON.stringify(input.sceneId)});
8683
- if (!scene) return { error: 'Scene not found: ${input.sceneId}' };
8847
+ if (!scene) return { error: 'Scene not found: ' + ${jsLiteral(input.sceneId)} };
8684
8848
  return { success: true, action: 'info', scene: scene };
8685
8849
  })();
8686
8850
  `.trim();
@@ -9144,7 +9308,7 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
9144
9308
  selected.push(id);
9145
9309
  }
9146
9310
  }
9147
- return { success: true, action: 'select', mode: '${mode}', selected };
9311
+ return { success: true, action: 'select', mode: ${jsLiteral(mode)}, selected };
9148
9312
  })();`.trim();
9149
9313
  }
9150
9314
  case 'select_all':
@@ -9201,7 +9365,7 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
9201
9365
  */
9202
9366
  _facadeCall(method, args, label) {
9203
9367
  return `
9204
- // ${label}
9368
+ // ${oneLine(label)}
9205
9369
  (async function() {
9206
9370
  if (typeof app.${method} !== 'function') {
9207
9371
  return { success: false, error: 'app.${method}() unavailable — update PinePaper Studio to a build that has it.' };
@@ -9223,7 +9387,7 @@ ${input.cutSound !== undefined ? ` const __cuts = (__cr && __cr.cuts) || (app.s
9223
9387
  // studios, and a brand must not cross between them.
9224
9388
  if (input.action === 'get' || input.action === 'clear') {
9225
9389
  return `
9226
- // Brand kit: ${input.action} the active brand
9390
+ // Brand kit: ${oneLine(input.action)} the active brand
9227
9391
  (function() {
9228
9392
  const b = (typeof window !== 'undefined' && window.__ppActiveBrand) || null;
9229
9393
  ${input.action === 'clear' ? 'if (typeof window !== "undefined") window.__ppActiveBrand = null;' : ''}
@@ -9480,7 +9644,7 @@ ${loads}
9480
9644
  __src = raster.url;
9481
9645
  }`;
9482
9646
  const call = (method, args, label) => `
9483
- // ${label}
9647
+ // ${oneLine(label)}
9484
9648
  (async function() {
9485
9649
  if (typeof app.${method} !== 'function') {
9486
9650
  return { success: false, error: 'app.${method}() unavailable — update PinePaper Studio to a build that has it.' };
@@ -9624,7 +9788,7 @@ ${resolveSource}
9624
9788
  case 'fit': {
9625
9789
  const mode = input.mode || 'contain';
9626
9790
  return `
9627
- // Fit item to the export frame (${mode})
9791
+ // Fit item to the export frame (${oneLine(mode)})
9628
9792
  (function() {
9629
9793
  // Route through the agent facade, not app.fitToFrame directly: an agent that
9630
9794
  // just called upload_video holds a MEDIA id (vraster_…), which is a different
@@ -9667,7 +9831,7 @@ ${resolveSource}
9667
9831
  case 'flip': {
9668
9832
  const dir = input.direction || 'horizontal';
9669
9833
  return `
9670
- // Flip item ${dir}ly
9834
+ // Flip item ${oneLine(dir)}ly
9671
9835
  (function() {
9672
9836
  const entry = app.itemRegistry.get(${ID});
9673
9837
  if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
@@ -9688,7 +9852,7 @@ ${resolveSource}
9688
9852
  if (needsSibling) {
9689
9853
  const siblingExpr = order === 'moveUp' ? 'entry.item.nextSibling' : 'entry.item.previousSibling';
9690
9854
  return `
9691
- // Reorder item: ${order}
9855
+ // Reorder item: ${oneLine(order)}
9692
9856
  (function() {
9693
9857
  const entry = app.itemRegistry.get(${ID});
9694
9858
  if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
@@ -9698,7 +9862,7 @@ ${resolveSource}
9698
9862
  })();`.trim();
9699
9863
  }
9700
9864
  return `
9701
- // Reorder item: ${order}
9865
+ // Reorder item: ${oneLine(order)}
9702
9866
  (function() {
9703
9867
  const entry = app.itemRegistry.get(${ID});
9704
9868
  if (!entry || !entry.item) return { error: 'Item not found: ' + ${ID} };
@@ -9788,7 +9952,7 @@ ${resolve}
9788
9952
  // otherwise; a falsy answer is a refusal it did not raise.
9789
9953
  if (r === false || r === null) {
9790
9954
  return { success: false, action: 'apply', itemId: ${JSON.stringify(input.itemId)},
9791
- error: 'the filter did not apply — ${input.filterName || ''} may not be available on this studio.' };
9955
+ error: 'the filter did not apply — ' + ${jsLiteral(input.filterName || '')} + ' may not be available on this studio.' };
9792
9956
  }
9793
9957
  return { success: true, action: 'apply', itemId: ${JSON.stringify(input.itemId)}, filter: ${JSON.stringify(input.filterName || '')} };
9794
9958
  } catch (e) {
@@ -10143,7 +10307,7 @@ ${resolve}
10143
10307
  // Get item details by ID
10144
10308
  (function() {
10145
10309
  const entry = app.itemRegistry.get(${JSON.stringify(input.itemId || '')});
10146
- if (!entry || !entry.item) return { error: 'Item not found: ${input.itemId}' };
10310
+ if (!entry || !entry.item) return { error: 'Item not found: ' + ${jsLiteral(input.itemId)} };
10147
10311
  const item = entry.item;
10148
10312
  return {
10149
10313
  success: true, action: 'get_by_id',
@@ -10443,8 +10607,8 @@ ${resolve}
10443
10607
  ${guard}
10444
10608
  const entry = app.itemRegistry.get(${itemIdStr});
10445
10609
  if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
10446
- app.deformPresets.apply(entry.item, '${input.preset}', ${JSON.stringify(params)});
10447
- return { success: true, action: 'apply', itemId: ${itemIdStr}, preset: '${input.preset}' };
10610
+ app.deformPresets.apply(entry.item, ${jsLiteral(input.preset)}, ${JSON.stringify(params)});
10611
+ return { success: true, action: 'apply', itemId: ${itemIdStr}, preset: ${jsLiteral(input.preset)} };
10448
10612
  })();`.trim();
10449
10613
  }
10450
10614
  case 'trigger': {
@@ -10464,8 +10628,8 @@ ${resolve}
10464
10628
  ${guard}
10465
10629
  const entry = app.itemRegistry.get(${itemIdStr});
10466
10630
  if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
10467
- app.deformPresets.trigger(entry.item, '${input.preset}', ${JSON.stringify(params)});
10468
- return { success: true, action: 'trigger', itemId: ${itemIdStr}, preset: '${input.preset}' };
10631
+ app.deformPresets.trigger(entry.item, ${jsLiteral(input.preset)}, ${JSON.stringify(params)});
10632
+ return { success: true, action: 'trigger', itemId: ${itemIdStr}, preset: ${jsLiteral(input.preset)} };
10469
10633
  })();`.trim();
10470
10634
  }
10471
10635
  case 'remove': {
@@ -10718,8 +10882,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
10718
10882
  // Export sprite sheet
10719
10883
  (async function() {
10720
10884
  ${guard}
10721
- const result = await app.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: '${input.format || 'png'}', download: true, includeMetadata: true });
10722
- return { success: true, action: 'export', format: '${input.format || 'png'}' };
10885
+ const result = await app.exportSpriteSheet(${JSON.stringify(input.spriteSheetId || '')}, { format: ${jsLiteral(input.format || 'png')}, download: true, includeMetadata: true });
10886
+ return { success: true, action: 'export', format: ${jsLiteral(input.format || 'png')} };
10723
10887
  })();`.trim();
10724
10888
  default:
10725
10889
  return `(function() { return { error: 'Unknown sprite sheet action: ${input.action}' }; })();`;
@@ -10740,8 +10904,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
10740
10904
  ${guard}
10741
10905
  const entry = app.itemRegistry.get(${itemIdStr});
10742
10906
  if (!entry || !entry.item) return { error: 'Item not found: ' + ${itemIdStr} };
10743
- const behaviorId = app.interactionSystem.addContinuousBehavior(entry.item, '${input.behaviorType}', ${params});
10744
- return { success: true, action: 'add_behavior', itemId: ${itemIdStr}, behaviorType: '${input.behaviorType}', behaviorId };
10907
+ const behaviorId = app.interactionSystem.addContinuousBehavior(entry.item, ${jsLiteral(input.behaviorType)}, ${params});
10908
+ return { success: true, action: 'add_behavior', itemId: ${itemIdStr}, behaviorType: ${jsLiteral(input.behaviorType)}, behaviorId };
10745
10909
  })();`.trim();
10746
10910
  }
10747
10911
  case 'remove_behavior': {
@@ -10798,8 +10962,8 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
10798
10962
  // Trigger interaction action
10799
10963
  (function() {
10800
10964
  ${guard}
10801
- app.interactionSystem.executeAction('${input.actionType}', ${params});
10802
- return { success: true, action: 'trigger_action', actionType: '${input.actionType}' };
10965
+ app.interactionSystem.executeAction(${jsLiteral(input.actionType)}, ${params});
10966
+ return { success: true, action: 'trigger_action', actionType: ${jsLiteral(input.actionType)} };
10803
10967
  })();`.trim();
10804
10968
  }
10805
10969
  case 'get_state':
@@ -11226,14 +11390,14 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11226
11390
  }[input.action];
11227
11391
  const id = JSON.stringify(input.itemId);
11228
11392
  return `
11229
- // Arrange ${input.itemId} → ${input.action}
11393
+ // Arrange ${oneLine(input.itemId)} → ${oneLine(input.action)}
11230
11394
  (function() {
11231
11395
  const item = app.getItemById(${id});
11232
11396
  if (!item) return { success: false, error: 'Item not found: ' + ${id} };
11233
11397
  if (typeof app.${method} !== 'function') return { success: false, error: 'app.${method} unavailable — update FxTool' };
11234
11398
  app.select(${id});
11235
11399
  app.${method}();
11236
- return { success: true, itemId: ${id}, action: '${input.action}' };
11400
+ return { success: true, itemId: ${id}, action: ${jsLiteral(input.action)} };
11237
11401
  })();`.trim();
11238
11402
  }
11239
11403
  /**
@@ -11266,7 +11430,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11266
11430
  ...(input.queries !== undefined ? { queries: input.queries } : {}),
11267
11431
  });
11268
11432
  return `
11269
- // Detect objects${input.queries !== undefined ? ' (open-vocabulary / OWL-ViT)' : ''}${input.asNodes ? ' (asNodes — relational compositing)' : ''}
11433
+ // Detect objects${oneLine(input.queries !== undefined ? ' (open-vocabulary / OWL-ViT)' : '')}${oneLine(input.asNodes ? ' (asNodes — relational compositing)' : '')}
11270
11434
  (async function() {
11271
11435
  if (typeof app.detectObjects !== 'function') {
11272
11436
  return { success: false, error: 'app.detectObjects unavailable — update FxTool to a build that exposes on-device object detection on app' };
@@ -11296,7 +11460,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11296
11460
  ...(input.id !== undefined ? { id: input.id } : {}),
11297
11461
  };
11298
11462
  return `
11299
- // Styled scene${input.style ? ` (${input.style.replace(/[\r\n]/g, ' ')})` : ''}
11463
+ // Styled scene${input.style ? ` (${oneLine(input.style.replace(/[\r\n]/g, ' '))})` : ''}
11300
11464
  (async function() {
11301
11465
  if (typeof app.styledScene !== 'function') { return { success: false, error: 'app.styledScene unavailable — update FxTool' }; }
11302
11466
  const r = await app.styledScene(${JSON.stringify(input.spec ?? {})}, ${JSON.stringify(opts)});
@@ -11316,7 +11480,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11316
11480
  ...(input.threshold !== undefined ? { threshold: input.threshold } : {}),
11317
11481
  });
11318
11482
  return `
11319
- // Extract object${input.label ? ` "${input.label}"` : ''} from the image
11483
+ // Extract object${input.label ? ` ${jsLiteral(input.label)}` : ''} from the image
11320
11484
  (async function() {
11321
11485
  if (typeof app.extractObject !== 'function') {
11322
11486
  return { success: false, error: 'app.extractObject unavailable — update FxTool to a build that exposes on-device object detection on app' };
@@ -11406,7 +11570,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11406
11570
  if (input.action === 'break_apart') {
11407
11571
  const idExpr = JSON.stringify(input.itemId || '');
11408
11572
  return `
11409
- // Break apart ${input.itemId || ''} into movable parts
11573
+ // Break apart ${oneLine(input.itemId || '')} into movable parts
11410
11574
  (function() {
11411
11575
  if (typeof app.breakApart !== 'function') return { success: false, error: 'app.breakApart unavailable — update FxTool to a build with SVG/group segmentation' };
11412
11576
  const target = app.getItemById(${idExpr});
@@ -11421,7 +11585,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11421
11585
  if (input.action === 'ungroup') {
11422
11586
  const idExpr = JSON.stringify(input.groupId || '');
11423
11587
  return `
11424
- // Ungroup ${input.groupId || ''}
11588
+ // Ungroup ${oneLine(input.groupId || '')}
11425
11589
  (function() {
11426
11590
  if (!app.groupManager) return { success: false, error: 'GroupManager not available — update FxTool' };
11427
11591
  const gid = ${idExpr};
@@ -11435,7 +11599,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11435
11599
  const idsArrayExpr = JSON.stringify(input.itemIds ?? []);
11436
11600
  const nameExpr = input.groupName !== undefined ? JSON.stringify(input.groupName) : 'undefined';
11437
11601
  return `
11438
- // Group ${(input.itemIds || []).length} items${input.groupName ? ` as "${input.groupName}"` : ''}
11602
+ // Group ${oneLine((input.itemIds || []).length)} items${input.groupName ? ` as ${jsLiteral(input.groupName)}` : ''}
11439
11603
  (function() {${this.groupCodeBody(idsArrayExpr, nameExpr)}
11440
11604
  })();`.trim();
11441
11605
  }
@@ -11485,7 +11649,7 @@ if (!app.spriteSystem) return { error: 'SpriteSheetSystem not available' };`;
11485
11649
  const duration = input.duration ?? 5;
11486
11650
  const n = Math.min(1200, Math.max(3, Math.round(duration * fps)));
11487
11651
  return `
11488
- // Accessibility measurements (${checks.join(', ')})
11652
+ // Accessibility measurements (${oneLine(checks.join(', '))})
11489
11653
  (async function() {
11490
11654
  const out = { success: true, fps: ${fps}, duration: ${duration} };
11491
11655
  const rgba = function(c) {
@@ -11642,7 +11806,7 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
11642
11806
  const mime = sheet.format === 'png' ? 'image/png' : 'image/jpeg';
11643
11807
  const S = (v) => JSON.stringify(v);
11644
11808
  return `
11645
- // Contact sheet of ${n} frame(s)
11809
+ // Contact sheet of ${oneLine(n)} frame(s)
11646
11810
  (async function() {
11647
11811
  if (typeof app.captureFramesAt !== 'function') {
11648
11812
  return { success: false, error: 'app.captureFramesAt unavailable — update FxTool to a build with the deterministic capture entrypoint' };
@@ -11718,7 +11882,7 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
11718
11882
  // tool call that screenshots next wants that to have happened.
11719
11883
  const engineOptsJson = JSON.stringify({ canvas: input.canvas, defaultGeometry: input.defaultGeometry, awaitImages: true });
11720
11884
  return `
11721
- // Instantiate ontology → scene (${ops.filter((o) => o.op === 'create').length} items, ${ops.filter((o) => o.op === 'addRelation').length} relations)
11885
+ // Instantiate ontology → scene (${oneLine(ops.filter((o) => o.op === 'create').length)} items, ${oneLine(ops.filter((o) => o.op === 'addRelation').length)} relations)
11722
11886
  (async function() {
11723
11887
  // Engine door first: the studio's own OntologyCompiler reads every node facet
11724
11888
  // (geometry, paint, text, the keyframe track, masks) and builds through the
@@ -11803,10 +11967,27 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
11803
11967
  return { success: false, error: 'app.lintScene unavailable — update FxTool to a build with the relational linter (S12-E2)' };
11804
11968
  }
11805
11969
  const result = app.lintScene(${optsJson});
11970
+ // THE RELATIONS AS BOUND, so a later agent can find the one to edit without exporting the scene to read it:
11971
+ // from, to, type and params, capped. And the custom relation definitions registered on this page, as given.
11972
+ const __rels = [];
11973
+ let __relCount = 0;
11974
+ try {
11975
+ const __rr = app.relationRegistry;
11976
+ const __active = __rr && typeof __rr.queryActive === 'function' ? __rr.queryActive() : [];
11977
+ __active.forEach((e) => (e.associations || []).forEach((a) => {
11978
+ __relCount++;
11979
+ if (__rels.length >= ${RELATIONS_READBACK_CAP}) return;
11980
+ const __name = e.item && e.item.data && e.item.data.name;
11981
+ __rels.push({ from: e.itemId, ...(__name ? { fromName: __name } : {}), to: a.target === undefined ? null : a.target, type: a.relation, params: a.params || {} });
11982
+ }));
11983
+ } catch (e) { /* the relations are an addition; the lint stands without them */ }
11984
+ const __custom = (typeof window !== 'undefined' && window.__ppCustomRelations) ? Object.values(window.__ppCustomRelations) : [];
11806
11985
  // warnings: what the engine's own rules found (TEXT_CLIPS_FRAME: a centre-anchored text running past the
11807
11986
  // frame edge). Passed through when the studio reports them; an older studio has none to pass.
11808
11987
  return { success: true, density: result.density, suggestions: result.suggestions,
11809
- ...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}) };
11988
+ ...(Array.isArray(result.warnings) && result.warnings.length ? { warnings: result.warnings } : {}),
11989
+ relations: __rels, ...(__relCount > __rels.length ? { relationsShown: __rels.length, relationCount: __relCount } : {}),
11990
+ ...(__custom.length ? { customRelations: __custom } : {}) };
11810
11991
  })();`.trim();
11811
11992
  }
11812
11993
  /**
@@ -11837,11 +12018,11 @@ ${checks.includes('contrast') ? ` // SAMPLED OVER TIME (round 12, 12.9): text i
11837
12018
  if (u.indexOf('data:') !== 0) return u;
11838
12019
  const comma = u.indexOf(',');
11839
12020
  const meta = u.slice(5, comma);
11840
- const mime = meta.split(';')[0] || '${kind === 'audio' ? 'audio/mpeg' : 'video/mp4'}';
12021
+ const mime = meta.split(';')[0] || ${jsLiteral(kind === 'audio' ? 'audio/mpeg' : 'video/mp4')};
11841
12022
  const body = u.slice(comma + 1);
11842
12023
  const bin = /;base64/i.test(meta) ? atob(body) : decodeURIComponent(body);
11843
12024
  const bytes = Uint8Array.from(bin, function(c) { return c.charCodeAt(0); });
11844
- const ext = (mime.split('/')[1] || '${kind === 'audio' ? 'mp3' : 'mp4'}').replace(/[^a-z0-9]/gi, '');
12025
+ const ext = (mime.split('/')[1] || ${jsLiteral(kind === 'audio' ? 'mp3' : 'mp4')}).replace(/[^a-z0-9]/gi, '');
11845
12026
  return new File([bytes], '${kind}.' + ext, { type: mime });
11846
12027
  })(${JSON.stringify(url ?? '')})`;
11847
12028
  // TWO ID SPACES, AND THE CALLER HOLDS THE WRONG ONE.
@@ -11950,7 +12131,7 @@ ${input.timeOffset !== undefined ? ` // Moving the clip is the 4th argument, wh
11950
12131
  // refusals come back as {ok:false, reason} and are errors here.
11951
12132
  case 'split':
11952
12133
  return `
11953
- // Split a media clip at canvas time ${input.at}
12134
+ // Split a media clip at canvas time ${oneLine(input.at)}
11954
12135
  (async function() {
11955
12136
  ${guard}
11956
12137
  if (typeof A.splitMedia !== 'function') { return { success: false, action: 'split', error: 'this studio is too old to split a clip (no splitMedia) — nothing was changed.' }; }
@@ -11960,7 +12141,7 @@ ${guard}
11960
12141
  })();`.trim();
11961
12142
  case 'add_transition':
11962
12143
  return `
11963
- // ${input.transition?.type ?? 'crossfade'} between ${input.fromItemId} and ${input.toItemId}
12144
+ // ${oneLine(input.transition?.type ?? 'crossfade')} between ${oneLine(input.fromItemId)} and ${oneLine(input.toItemId)}
11964
12145
  (function() {
11965
12146
  ${guard}
11966
12147
  if (typeof A.addTransition !== 'function') { return { success: false, action: 'add_transition', error: 'this studio is too old for clip transitions (no addTransition) — nothing was changed.' }; }
@@ -12082,7 +12263,7 @@ ${guard}
12082
12263
  ...(input.live !== undefined ? { live: input.live } : {}),
12083
12264
  });
12084
12265
  return `
12085
- // Track matte: the matte item's ${input.channel || 'luma'} drives the target's alpha${input.live ? ' (LIVE — re-cuts as the matte animates)' : ''}
12266
+ // Track matte: the matte item's ${oneLine(input.channel || 'luma')} drives the target's alpha${oneLine(input.live ? ' (LIVE — re-cuts as the matte animates)' : '')}
12086
12267
  (async function() {
12087
12268
  // A media id (vraster_ / araster_, what upload returns) is resolved to the
12088
12269
  // registry id these app facades take (round 10, 10.12).
@@ -12167,7 +12348,7 @@ ${guard}
12167
12348
  ...(input.fontSize !== undefined ? { fontSize: input.fontSize } : {}),
12168
12349
  });
12169
12350
  return `
12170
- // Display text style: ${input.styleKey} — stacked-layer title, id adopted from the text
12351
+ // Display text style: ${oneLine(input.styleKey)} — stacked-layer title, id adopted from the text
12171
12352
  (async function() {
12172
12353
  if (typeof app.applyTextStyle !== 'function') { return { success: false, error: 'app.applyTextStyle unavailable — update FxTool to a text-styles build' }; }
12173
12354
  // LOAD THE FACES FIRST. The decorative stylesheet is fetched on pointerenter
@@ -12318,7 +12499,7 @@ ${guard}
12318
12499
  const S = (v) => JSON.stringify(v);
12319
12500
  const id = S(input.itemId);
12320
12501
  return `
12321
- // Choreograph ${input.beats.length} beat(s) for ${input.itemId}
12502
+ // Choreograph ${oneLine(input.beats.length)} beat(s) for ${oneLine(input.itemId)}
12322
12503
  (function() {
12323
12504
  const __it = (app.getItemById && app.getItemById(${id}))
12324
12505
  || (app.itemRegistry && app.itemRegistry.get && (app.itemRegistry.get(${id}) || {}).item);
@@ -12426,7 +12607,7 @@ ${input.sound ? ` // SOUND ON THE ACTION (D68): one placed sound per moment the
12426
12607
  const cam = input.camera === true ? { fill: 0.45 } : (input.camera || null);
12427
12608
  const sound = input.sound === true ? 'whoosh' : (input.sound || null);
12428
12609
  return `
12429
- // Morph sequence: ${states.length} state(s)
12610
+ // Morph sequence: ${oneLine(states.length)} state(s)
12430
12611
  (function() {
12431
12612
  const src = app.getItemById && app.getItemById(${S(input.itemId)});
12432
12613
  if (!src) { return { success: false, error: ${S(input.itemId)} + ' is not on the canvas.' }; }
@@ -12903,7 +13084,7 @@ ${cam ? ` // The camera rides the SAME timing: framing each state as its morph
12903
13084
  }
12904
13085
  })();
12905
13086
  return `
12906
- // Shader graph: ${input.action}
13087
+ // Shader graph: ${oneLine(input.action)}
12907
13088
  (async function() {
12908
13089
  ${head}
12909
13090
  ${body}
@@ -12924,7 +13105,7 @@ ${body}
12924
13105
  // or brush-only medium with the reason. The Paper item it returns is not
12925
13106
  // passed back (it cannot cross the bridge); its id is.
12926
13107
  return `
12927
- // Medium: apply ${input.medium}
13108
+ // Medium: apply ${oneLine(input.medium)}
12928
13109
  (async function() {
12929
13110
  if (typeof app.applyMedium !== 'function') { return { success: false, error: 'app.applyMedium unavailable — update FxTool' }; }
12930
13111
  const r = await app.applyMedium(${S(input.itemId)}, ${S(input.medium)}, ${S(input.options ?? {})});
@@ -13118,7 +13299,7 @@ ${body}
13118
13299
  ...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}),
13119
13300
  });
13120
13301
  return `
13121
- // Text effect: ${input.effect} — the text becomes one animated item PER CHARACTER
13302
+ // Text effect: ${oneLine(input.effect)} — the text becomes one animated item PER CHARACTER
13122
13303
  (async function() {
13123
13304
  if (typeof app.applyTextEffect !== 'function') { return { success: false, error: 'app.applyTextEffect unavailable — update FxTool to a text-effects build' }; }
13124
13305
  const r = await app.applyTextEffect(${S(input.itemId)}, ${S(input.effect)}, ${opts});
@@ -13190,7 +13371,7 @@ ${body}
13190
13371
  ...(input.keepSource !== undefined ? { keepSource: input.keepSource } : {}),
13191
13372
  });
13192
13373
  return `
13193
- // Shatter ${input.itemId} into tiles (inert until animated — that is the point)
13374
+ // Shatter ${oneLine(input.itemId)} into tiles (inert until animated — that is the point)
13194
13375
  (function() {
13195
13376
  if (typeof app.shatterImage !== 'function') { return { success: false, error: 'app.shatterImage unavailable — update FxTool to a shatter-capable build' }; }
13196
13377
  const r = app.shatterImage(${S(input.itemId)}, ${opts});
@@ -13325,7 +13506,10 @@ ${body}
13325
13506
  const needWorld = ` if (!app._world3d) { return { success: false, error: "no 3D world — call pinepaper_world3d create first" }; }`;
13326
13507
  switch (input.action) {
13327
13508
  case 'create': {
13328
- const opts = S({ ...(input.character !== undefined ? { character: input.character } : {}) });
13509
+ if (input.control === true && input.character === false) {
13510
+ return `({ success: false, error: ${jsLiteral('control: true walks the character, and character: false leaves it out. Nothing was made. Drop one of the two.')} });`;
13511
+ }
13512
+ const opts = S({ ...(input.character !== undefined ? { character: input.character } : {}), ...(input.control === true ? { control: true } : {}) });
13329
13513
  return `
13330
13514
  // World3D: create — terrain, sky, shadows, walkable character, under the Paper canvas
13331
13515
  (async function() {
@@ -13338,7 +13522,11 @@ ${body}
13338
13522
  // params is WORLD_SCHEMA: an ARRAY of {path, kind, doc}. Object.keys on it
13339
13523
  // returned "0", "1", "2"… — the indexes, not a single usable key.
13340
13524
  const keys = !d ? [] : Array.isArray(d.params) ? d.params.map(function (e) { return e && e.path; }).filter(Boolean) : Object.keys(d.params || d);
13341
- return { success: true, action: 'create', spec: ${S(input.spec ?? 'forest')}, configurableKeys: keys };
13525
+ // What the world reports, not what was asked: an engine without the option takes the world and ignores control.
13526
+ const result = { success: true, action: 'create', spec: ${S(input.spec ?? 'forest')}, configurableKeys: keys };${input.control === true ? `
13527
+ result.control = !!(app._world3d && app._world3d.controlEnabled);
13528
+ if (!result.control) result.warning = 'control was asked for and this studio did not turn it on: the character cannot be walked with the keyboard. Update PinePaper Studio.';` : ''}
13529
+ return result;
13342
13530
  })();`.trim();
13343
13531
  }
13344
13532
  case 'describe':
@@ -13368,7 +13556,7 @@ ${needWorld}
13368
13556
  ...(input.live !== undefined ? { live: input.live } : {}),
13369
13557
  });
13370
13558
  return `
13371
- // World3D: put an actor on the stage${input.live ? ' (live sprite — a rigged character PERFORMS, not a photograph of itself)' : ''}
13559
+ // World3D: put an actor on the stage${oneLine(input.live ? ' (live sprite — a rigged character PERFORMS, not a photograph of itself)' : '')}
13372
13560
  (function() {
13373
13561
  if (typeof app.addWorldActor !== 'function') { return { success: false, error: 'app.addWorldActor unavailable — update FxTool' }; }
13374
13562
  ${needWorld}
@@ -13471,7 +13659,7 @@ ${needWorld}
13471
13659
  mesh.uniforms = uniforms;
13472
13660
  }
13473
13661
  return `
13474
- // World3D: ${input.action} — a canvas path becomes real geometry
13662
+ // World3D: ${oneLine(input.action)} — a canvas path becomes real geometry
13475
13663
  (async function() {
13476
13664
  // @engine-methods extrudeToMesh latheToMesh importOBJToWorld importGLTFToWorld
13477
13665
  if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool to a world3d-capable build' }; }
@@ -13578,7 +13766,7 @@ ${needWorld}
13578
13766
  case 'import_gltf': {
13579
13767
  const fn = input.action === 'import_obj' ? 'importOBJToWorld' : 'importGLTFToWorld';
13580
13768
  return `
13581
- // World3D: ${input.action}
13769
+ // World3D: ${oneLine(input.action)}
13582
13770
  (async function() {
13583
13771
  // @engine-methods extrudeToMesh latheToMesh importOBJToWorld importGLTFToWorld
13584
13772
  if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool to a world3d-capable build' }; }
@@ -13780,7 +13968,7 @@ ${needWorld}
13780
13968
  generateDesignCompose(scene, ops, style, sizeWarning, report = {}) {
13781
13969
  const S = (v) => JSON.stringify(v);
13782
13970
  return `
13783
- // Design: compose a ${style} scene — ${ops.length} items, laid out server-side
13971
+ // Design: compose a ${oneLine(style)} scene — ${oneLine(ops.length)} items, laid out server-side
13784
13972
  (function() {
13785
13973
  if (typeof app.create !== 'function') { return { success: false, error: 'app.create unavailable — update FxTool' }; }
13786
13974
  const size = ${S({ width: scene.width, height: scene.height })};
@@ -13860,7 +14048,7 @@ ${needWorld}
13860
14048
  }
13861
14049
  const fn = action === 'figure' ? 'stickFigure' : 'stickSet';
13862
14050
  return `
13863
- // Stick: ${action}
14051
+ // Stick: ${oneLine(action)}
13864
14052
  (async function() {
13865
14053
  // @engine-methods stickFigure stickSet
13866
14054
  if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool (the stick kit is vendored from mcp-cloud)' }; }
@@ -13954,7 +14142,7 @@ ${needWorld}
13954
14142
  case 'export_dotlottie': {
13955
14143
  const fn = input.action === 'export_lottie' ? 'exportLottie' : 'exportDotLottie';
13956
14144
  return `
13957
- // Interchange: ${fn}
14145
+ // Interchange: ${oneLine(fn)}
13958
14146
  (async function() {
13959
14147
  ${guard(fn)}
13960
14148
  ${TO_DATA_URL_JS}
@@ -14071,7 +14259,7 @@ ${guard('exportPNGSequence')}
14071
14259
  const S = (v) => JSON.stringify(v);
14072
14260
  const guard = (fn) => ` if (typeof app.${fn} !== 'function') { return { success: false, error: 'app.${fn} unavailable — update FxTool' }; }`;
14073
14261
  const simple = (fn, args, key, label) => key === 'played' ? `
14074
- // Sound: ${label}
14262
+ // Sound: ${oneLine(label)}
14075
14263
  (function() {
14076
14264
  ${guard(fn)}
14077
14265
  // A play that returns no voice played nothing, and reported success (gate
@@ -14080,7 +14268,7 @@ ${guard(fn)}
14080
14268
  if (__v == null) { return { success: false, error: 'nothing played: the studio returned no voice (it may have no audio output). Placed sounds (create, sequence) and render_soundtrack work without one.' }; }
14081
14269
  return { success: true, played: __v };
14082
14270
  })();`.trim() : `
14083
- // Sound: ${label}
14271
+ // Sound: ${oneLine(label)}
14084
14272
  (function() {
14085
14273
  ${guard(fn)}
14086
14274
  return { success: true, ${key}: app.${fn}(${args}) };
@@ -14102,7 +14290,7 @@ ${guard(fn)}
14102
14290
  : simple('playSoundFromText', S(input.text), 'played', 'a described sound');
14103
14291
  case 'from_text':
14104
14292
  return `
14105
- // Sound: resolve "${input.text}" to a spec WITHOUT playing it
14293
+ // Sound: resolve ${jsLiteral(input.text)} to a spec WITHOUT playing it
14106
14294
  (function() {
14107
14295
  ${guard('buildSoundFromText')}
14108
14296
  const spec = app.buildSoundFromText(${S(input.text)});
@@ -14145,7 +14333,7 @@ ${guard('buildSoundFromText')}
14145
14333
  // checking the method a caller needs. It extracted only
14146
14334
  // renderSoundtrackWav until this line existed.
14147
14335
  return `
14148
- // Sound: define ${input.name} at runtime
14336
+ // Sound: define ${oneLine(input.name)} at runtime
14149
14337
  (function() {
14150
14338
  ${guard(fn)}
14151
14339
  var name;
@@ -14329,7 +14517,7 @@ ${guard('timbreFromPath')}
14329
14517
  opts[k] = input[k];
14330
14518
  }
14331
14519
  return `
14332
- // Motion: ${input.motion} on ${input.itemIds?.length ? `${input.itemIds.length} items` : input.itemId}
14520
+ // Motion: ${oneLine(input.motion)} on ${input.itemIds?.length ? `${oneLine(input.itemIds.length)} items` : input.itemId}
14333
14521
  (async function() {
14334
14522
  if (typeof app.animateItems !== 'function') { return { success: false, error: 'app.animateItems unavailable — update FxTool' }; }
14335
14523
  const r = await app.animateItems(${target}, ${S(opts)});
@@ -14367,7 +14555,7 @@ ${guard('timbreFromPath')}
14367
14555
  switch (input.action) {
14368
14556
  case 'boolean':
14369
14557
  return `
14370
- // Path boolean: ${input.op} — CONSUMES its operands and leaves one result
14558
+ // Path boolean: ${oneLine(input.op)} — CONSUMES its operands and leaves one result
14371
14559
  (function() {
14372
14560
  ${guard('booleanOp')}${pass(`app.booleanOp(${S(input.itemIds)}, ${S(input.op)})`)}
14373
14561
  })();`.trim();
@@ -14391,7 +14579,7 @@ ${guard('togglePathClosed')}${pass(`app.togglePathClosed(${S(input.itemId)})`)}
14391
14579
  })();`.trim();
14392
14580
  case 'pattern':
14393
14581
  return `
14394
- // Pattern: repeat the item — ${input.kind}
14582
+ // Pattern: repeat the item — ${oneLine(input.kind)}
14395
14583
  (function() {
14396
14584
  ${guard('patternFrom')}${pass(`app.patternFrom(${S(input.itemId)}, ${S(input.kind)}, ${S(input.pattern ?? {})})`)}
14397
14585
  })();`.trim();
@@ -14427,7 +14615,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
14427
14615
  generateCropImage(input) {
14428
14616
  const opts = input.aspectRatio ? JSON.stringify({ aspectRatio: input.aspectRatio }) : '{}';
14429
14617
  return `
14430
- // Crop image ${input.itemId}
14618
+ // Crop image ${oneLine(input.itemId)}
14431
14619
  (async function() {
14432
14620
  if (typeof app.cropImage !== 'function') { return { success: false, error: 'app.cropImage unavailable — update FxTool to a build with one-shot image ops' }; }
14433
14621
  const result = await app.cropImage(${JSON.stringify(input.itemId)}, ${JSON.stringify(input.rect)}, ${opts});
@@ -14446,7 +14634,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
14446
14634
  ...(input.smoothing !== undefined ? { smoothing: input.smoothing } : {}),
14447
14635
  });
14448
14636
  return `
14449
- // Chroma-key image ${input.itemId}
14637
+ // Chroma-key image ${oneLine(input.itemId)}
14450
14638
  (async function() {
14451
14639
  if (typeof app.applyChromaKey !== 'function') { return { success: false, error: 'app.applyChromaKey unavailable — update FxTool to a build with one-shot image ops' }; }
14452
14640
  const result = await app.applyChromaKey(${JSON.stringify(input.itemId)}, ${opts});
@@ -14862,7 +15050,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
14862
15050
  const argsJson = JSON.stringify(input.args ?? []);
14863
15051
  const createAsJson = input.createAs ? JSON.stringify(input.createAs) : 'null';
14864
15052
  return `
14865
- // Geometry construction: ${input.operation}
15053
+ // Geometry construction: ${oneLine(input.operation)}
14866
15054
  (function() {
14867
15055
  const g = app.geometry;
14868
15056
  if (!g || typeof g[${op}] !== 'function') {
@@ -14924,7 +15112,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
14924
15112
  const optsJson = JSON.stringify(input);
14925
15113
  const kindJson = JSON.stringify(input.kind);
14926
15114
  return `
14927
- // Equation-driven path: ${input.kind}${input.preset ? ' ' + input.preset : ''}
15115
+ // Equation-driven path: ${oneLine(input.kind)}${oneLine(input.preset ? ' ' + input.preset : '')}
14928
15116
  (function() {
14929
15117
  if (typeof app.createEquationPath !== 'function') {
14930
15118
  return { success: false, error: 'app.createEquationPath unavailable — update FxTool to a build with the Expression IR equation-path API (S10 B5)' };
@@ -14948,7 +15136,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
14948
15136
  const nameJson = JSON.stringify(input.name);
14949
15137
  const optsJson = JSON.stringify({ payloadType: input.payloadType, x: input.x, y: input.y, ...(input.at !== undefined ? { at: input.at } : {}) });
14950
15138
  return `
14951
- // Create event: ${input.name}
15139
+ // Create event: ${oneLine(input.name)}
14952
15140
  (function() {
14953
15141
  if (typeof app.createEvent !== 'function') {
14954
15142
  return { success: false, error: 'app.createEvent unavailable — update FxTool to a build with the event system (S11)' };
@@ -14963,7 +15151,7 @@ ${guard('unlockAllItems')}${pass('app.unlockAllItems()')}
14963
15151
  const eventIdJson = JSON.stringify(input.eventId);
14964
15152
  const payloadJson = JSON.stringify(input.payload ?? null);
14965
15153
  return `
14966
- // Pulse event: ${input.eventId}
15154
+ // Pulse event: ${oneLine(input.eventId)}
14967
15155
  (function() {
14968
15156
  if (typeof app.pulseEvent !== 'function') {
14969
15157
  return { success: false, error: 'app.pulseEvent unavailable — update FxTool to a build with the event system (S11)' };
@@ -15108,7 +15296,7 @@ return {
15108
15296
  */
15109
15297
  export function generateAutoKeyCode(itemId, props, atTime, easing) {
15110
15298
  return `
15111
- // Auto-key ${itemId} at ${atTime}s
15299
+ // Auto-key ${oneLine(itemId)} at ${oneLine(atTime)}s
15112
15300
  (function() {
15113
15301
  const item = app.getItemById(${JSON.stringify(itemId)});
15114
15302
  if (!item) return { success: false, error: ${JSON.stringify(`no item "${itemId}" on the canvas — check the id with pinepaper_get_items.`)} };
@@ -15166,7 +15354,7 @@ export function generateAutoKeyCode(itemId, props, atTime, easing) {
15166
15354
  */
15167
15355
  export function generateQueryMutationsCode(itemId, property, range, fps = 30) {
15168
15356
  return `
15169
- // Keyframe series for ${itemId}${property ? '.' + property : ''}
15357
+ // Keyframe series for ${oneLine(itemId)}${oneLine(property ? '.' + property : '')}
15170
15358
  (function() {
15171
15359
  const item = app.getItemById(${JSON.stringify(itemId)});
15172
15360
  if (!item) return { success: false, error: ${JSON.stringify(`no item "${itemId}" on the canvas.`)} };