editmamei 0.22.4 → 0.24.1

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 (80) hide show
  1. package/README.md +8 -6
  2. package/dist/api/extendscript/_helpers.js +97 -13
  3. package/dist/api/photoshop-api.js +4 -3
  4. package/dist/api/snippet-client.js +38 -2
  5. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  6. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  7. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  8. package/dist/core/server.js +56 -180
  9. package/dist/delivery/client.js +50 -1
  10. package/dist/delivery/provision.js +18 -1
  11. package/dist/delivery/signing.js +38 -1
  12. package/dist/delivery/store.js +149 -16
  13. package/dist/detection/detect-active-doc.js +41 -10
  14. package/dist/detection/detection-client.js +3 -3
  15. package/dist/detection/face-detector.js +2 -2
  16. package/dist/detection/geometry.js +14 -0
  17. package/dist/detection/landmark-detection-client.js +3 -3
  18. package/dist/detection/mesh-face.js +2 -3
  19. package/dist/detection/object-detector.js +2 -2
  20. package/dist/detection/runtime.js +14 -3
  21. package/dist/detection/sam-segmenter.js +69 -26
  22. package/dist/kernel/kernel.js +5 -9
  23. package/dist/kernel/module-lifecycle.js +181 -0
  24. package/dist/license/entitlement.js +13 -3
  25. package/dist/license/store.js +8 -1
  26. package/dist/modules/ce/index.js +1 -1
  27. package/dist/perception/facets.js +19 -1
  28. package/dist/perception/grounding-anchors.js +2 -12
  29. package/dist/perception/grounding-locate.js +1 -9
  30. package/dist/perception/grounding-review-crop.js +4 -29
  31. package/dist/perception/overlay.js +62 -0
  32. package/dist/perception/pixel-identity.js +41 -0
  33. package/dist/perception/region-precompute.js +85 -17
  34. package/dist/perception/scene-model.js +93 -50
  35. package/dist/perception/select-recipes.js +107 -66
  36. package/dist/perception/sky-ground-flood.js +0 -5
  37. package/dist/perception/sky-mask-transfer.js +72 -13
  38. package/dist/platform/connection.js +42 -11
  39. package/dist/platform/executor-queue.js +68 -0
  40. package/dist/platform/macos-detector.js +2 -1
  41. package/dist/platform/macos-executor.js +18 -67
  42. package/dist/platform/run-child.js +6 -3
  43. package/dist/platform/windows-detector.js +14 -7
  44. package/dist/platform/windows-executor.js +9 -64
  45. package/dist/skills/editmamei-skill.zip +0 -0
  46. package/dist/tools/adjustment-tools.js +78 -130
  47. package/dist/tools/brush-tools.js +3 -14
  48. package/dist/tools/channel-compose-tools.js +4 -23
  49. package/dist/tools/detection-tools.js +59 -47
  50. package/dist/tools/document-tools.js +121 -131
  51. package/dist/tools/filter-tools.js +489 -730
  52. package/dist/tools/group-tools.js +90 -156
  53. package/dist/tools/guide-tools.js +52 -83
  54. package/dist/tools/history-tools.js +29 -63
  55. package/dist/tools/image-placement-tools.js +18 -37
  56. package/dist/tools/image-tools.js +23 -51
  57. package/dist/tools/layer-ordering-tools.js +2 -9
  58. package/dist/tools/layer-properties-tools.js +245 -384
  59. package/dist/tools/layer-tools.js +115 -181
  60. package/dist/tools/layer-transform-tools.js +41 -106
  61. package/dist/tools/metadata-tools.js +2 -9
  62. package/dist/tools/path-tools.js +3 -14
  63. package/dist/tools/portrait-tools.js +2 -5
  64. package/dist/tools/preview-tools.js +53 -65
  65. package/dist/tools/retouch-tools.js +32 -70
  66. package/dist/tools/scene-tools.js +94 -73
  67. package/dist/tools/selection-tools.js +306 -563
  68. package/dist/tools/shape-tools.js +18 -10
  69. package/dist/tools/text-tools.js +24 -57
  70. package/dist/tools/transform-canvas-tools.js +21 -54
  71. package/dist/tools/vector-mask-tools.js +2 -9
  72. package/dist/utils/jpeg-quality.js +1 -0
  73. package/dist/utils/logger.js +3 -0
  74. package/dist/utils/operation-timeouts.js +7 -0
  75. package/dist/utils/run-script.js +7 -2
  76. package/dist/utils/session-log.js +134 -12
  77. package/dist/utils/template-storage.js +5 -6
  78. package/dist/utils/tool-helpers.js +37 -0
  79. package/dist/version.js +1 -1
  80. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
+ import { toolErrorResult, applyToActiveLayerProp } from '../utils/tool-helpers.js';
3
4
  const BLEND_MODES = [
4
5
  'normal',
5
6
  'multiply',
@@ -45,11 +46,7 @@ const applyImageSchema = {
45
46
  maximum: 100,
46
47
  default: 100,
47
48
  },
48
- apply_to_active_layer: {
49
- type: 'boolean',
50
- description: 'If false (default) the composite is baked onto a DUPLICATE of the active layer (original preserved — delete the copy to revert). Set true to bake destructively in place.',
51
- default: false,
52
- },
49
+ apply_to_active_layer: applyToActiveLayerProp('the composite'),
53
50
  },
54
51
  required: ['blend'],
55
52
  };
@@ -182,15 +179,7 @@ async function applyImage(connection, snippetClient, rawArgs) {
182
179
  };
183
180
  }
184
181
  catch (error) {
185
- return {
186
- content: [
187
- {
188
- type: 'text',
189
- text: `Error in ps_apply_image: ${error instanceof Error ? error.message : String(error)}`,
190
- },
191
- ],
192
- isError: true,
193
- };
182
+ return toolErrorResult('Error in ps_apply_image', error);
194
183
  }
195
184
  }
196
185
  async function calculations(connection, snippetClient, rawArgs) {
@@ -228,14 +217,6 @@ async function calculations(connection, snippetClient, rawArgs) {
228
217
  };
229
218
  }
230
219
  catch (error) {
231
- return {
232
- content: [
233
- {
234
- type: 'text',
235
- text: `Error in ps_calculations: ${error instanceof Error ? error.message : String(error)}`,
236
- },
237
- ],
238
- isError: true,
239
- };
220
+ return toolErrorResult('Error in ps_calculations', error);
240
221
  }
241
222
  }
@@ -1,7 +1,11 @@
1
- import { decode, encode } from 'jpeg-js';
1
+ import { encode } from 'jpeg-js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
3
  import { detectActiveDoc } from '../detection/detect-active-doc.js';
4
+ import { ANNOTATED_PREVIEW_JPEG_QUALITY } from '../utils/jpeg-quality.js';
4
5
  import { OnnxDetectionClient, } from '../detection/detection-client.js';
6
+ import { toolErrorResult } from '../utils/tool-helpers.js';
7
+ import { computePixelIdentity, docKeyFrom, samePixelIdentity, } from '../perception/pixel-identity.js';
8
+ import { ANNOTATION_RGB, annotationThickness, drawBoxOutline, } from '../perception/overlay.js';
5
9
  const detectSchema = {
6
10
  type: 'object',
7
11
  properties: {
@@ -46,36 +50,15 @@ const detectSchema = {
46
50
  },
47
51
  },
48
52
  };
49
- function drawBoxes(bytes, boxes) {
50
- const img = decode(bytes, { useTArray: true, formatAsRGBA: true });
51
- const { width: w, height: h, data } = img;
52
- const thickness = Math.max(2, Math.round(Math.max(w, h) / 400));
53
- const setPx = (x, y, r, g, b) => {
54
- if (x < 0 || y < 0 || x >= w || y >= h)
55
- return;
56
- const i = (y * w + x) * 4;
57
- data[i] = r;
58
- data[i + 1] = g;
59
- data[i + 2] = b;
60
- data[i + 3] = 255;
61
- };
53
+ export function drawBoxes(img, boxes) {
54
+ const { width: w, height: h } = img;
55
+ const data = Uint8Array.from(img.data);
56
+ const out = { data, width: w, height: h };
57
+ const thickness = annotationThickness(out);
62
58
  for (const { bbox, rgb } of boxes) {
63
- const x1 = Math.round(bbox[0]);
64
- const y1 = Math.round(bbox[1]);
65
- const x2 = Math.round(bbox[2]);
66
- const y2 = Math.round(bbox[3]);
67
- for (let x = x1; x <= x2; x++)
68
- for (let k = 0; k < thickness; k++) {
69
- setPx(x, y1 + k, rgb[0], rgb[1], rgb[2]);
70
- setPx(x, y2 - k, rgb[0], rgb[1], rgb[2]);
71
- }
72
- for (let y = y1; y <= y2; y++)
73
- for (let k = 0; k < thickness; k++) {
74
- setPx(x1 + k, y, rgb[0], rgb[1], rgb[2]);
75
- setPx(x2 - k, y, rgb[0], rgb[1], rgb[2]);
76
- }
59
+ drawBoxOutline(out, bbox, rgb, thickness);
77
60
  }
78
- return encode({ data, width: w, height: h }, 88).data;
61
+ return encode({ data, width: w, height: h }, ANNOTATED_PREVIEW_JPEG_QUALITY).data;
79
62
  }
80
63
  function summarizeObjects(objects) {
81
64
  if (!objects || objects.length === 0)
@@ -85,13 +68,27 @@ function summarizeObjects(objects) {
85
68
  counts.set(o.label, (counts.get(o.label) ?? 0) + 1);
86
69
  return [...counts.entries()].map(([l, n]) => (n > 1 ? `${l}×${n}` : l)).join(', ');
87
70
  }
88
- async function detect(connection, client, rawArgs) {
71
+ let detectCache = null;
72
+ export function __clearDetectCache() {
73
+ detectCache = null;
74
+ }
75
+ async function detect(connection, client, rawArgs, detectDeps) {
89
76
  try {
90
77
  const args = validateArgs(detectSchema, rawArgs);
91
78
  const target = args.target ?? 'both';
92
79
  const annotate = args.annotate ?? true;
93
80
  const wantFaces = target === 'faces' || target === 'both';
94
81
  const wantObjects = target === 'objects' || target === 'both';
82
+ const detectKey = JSON.stringify({
83
+ faces: wantFaces,
84
+ objects: wantObjects,
85
+ maxDimension: args.max_dimension ?? null,
86
+ faceThreshold: args.face_threshold ?? null,
87
+ objectThreshold: args.object_threshold ?? null,
88
+ maxObjects: args.max_objects ?? null,
89
+ });
90
+ let hit = null;
91
+ let pendingIdentity = null;
95
92
  const det = await detectActiveDoc(connection, client, {
96
93
  faces: wantFaces,
97
94
  objects: wantObjects,
@@ -99,22 +96,41 @@ async function detect(connection, client, rawArgs) {
99
96
  faceThreshold: args.face_threshold,
100
97
  objectThreshold: args.object_threshold,
101
98
  maxObjects: args.max_objects,
102
- });
103
- const mapped = det.result;
99
+ shouldDetect: ({ decoded, context, docWidth, docHeight }) => {
100
+ if (!decoded)
101
+ return true;
102
+ const identity = computePixelIdentity(decoded, docKeyFrom(context, docWidth, docHeight));
103
+ if (detectCache &&
104
+ detectCache.optionsKey === detectKey &&
105
+ samePixelIdentity(detectCache.identity, identity)) {
106
+ hit = detectCache;
107
+ return false;
108
+ }
109
+ pendingIdentity = identity;
110
+ return true;
111
+ },
112
+ }, detectDeps);
113
+ const mapped = hit ? hit.result : det.result;
114
+ const rawDetections = hit ? hit.raw : det.raw;
115
+ if (!hit && pendingIdentity) {
116
+ detectCache = {
117
+ identity: pendingIdentity,
118
+ optionsKey: detectKey,
119
+ result: det.result,
120
+ raw: det.raw,
121
+ };
122
+ }
104
123
  const content = [];
105
- if (annotate) {
124
+ if (annotate && det.decoded) {
106
125
  try {
107
126
  const boxes = [
108
- ...(det.raw.faces ?? []).map((f) => ({
109
- bbox: f.bbox,
110
- rgb: [0, 220, 255],
111
- })),
112
- ...(det.raw.objects ?? []).map((o) => ({
127
+ ...(rawDetections.faces ?? []).map((f) => ({ bbox: f.bbox, rgb: ANNOTATION_RGB.face })),
128
+ ...(rawDetections.objects ?? []).map((o) => ({
113
129
  bbox: o.bbox,
114
- rgb: [255, 0, 220],
130
+ rgb: ANNOTATION_RGB.object,
115
131
  })),
116
132
  ];
117
- const annotated = drawBoxes(det.exportBytes, boxes);
133
+ const annotated = drawBoxes(det.decoded, boxes);
118
134
  content.push({
119
135
  type: 'image',
120
136
  data: annotated.toString('base64'),
@@ -150,14 +166,10 @@ async function detect(connection, client, rawArgs) {
150
166
  };
151
167
  }
152
168
  catch (error) {
153
- const msg = error instanceof Error ? error.message : String(error);
154
- return {
155
- content: [{ type: 'text', text: `Error during detection: ${msg}` }],
156
- isError: true,
157
- };
169
+ return toolErrorResult('Error during detection', error);
158
170
  }
159
171
  }
160
- export function createDetectionTools(connection, _snippetClient, client = new OnnxDetectionClient()) {
172
+ export function createDetectionTools(connection, _snippetClient, client = new OnnxDetectionClient(), detectDeps) {
161
173
  return [
162
174
  {
163
175
  tool: {
@@ -203,7 +215,7 @@ export function createDetectionTools(connection, _snippetClient, client = new On
203
215
  idempotentHint: true,
204
216
  },
205
217
  },
206
- handler: async (args) => detect(connection, client, args),
218
+ handler: async (args) => detect(connection, client, args, detectDeps),
207
219
  },
208
220
  ];
209
221
  }
@@ -1,5 +1,10 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
+ import { OPEN_DOCUMENT_TIMEOUT_MS, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS, } from '../utils/operation-timeouts.js';
4
+ import { toolErrorResult, runSnippetTool } from '../utils/tool-helpers.js';
5
+ import { purgeSceneChannels } from '../perception/region-precompute.js';
6
+ import { Logger } from '../utils/logger.js';
7
+ const logger = new Logger('document-tools');
3
8
  const createDocumentSchema = {
4
9
  type: 'object',
5
10
  properties: {
@@ -68,6 +73,11 @@ const savePsdSchema = {
68
73
  description: 'Include flattened composite for compatibility with other apps. Default true.',
69
74
  default: true,
70
75
  },
76
+ keep_scene_channels: {
77
+ type: 'boolean',
78
+ description: "Keep the managed scene:* alpha channels ps_read_scene precomputes. Default false — they are DERIVED masks (rebuilt by the next scene read) and each is a full-resolution channel, so baking them into the .psd bloats it badly (~51MB per channel on a 51MP document). Set true only if you want the saved file to carry the masks. The result reports scene_channels_purged either way, so it's never silent.",
79
+ default: false,
80
+ },
71
81
  },
72
82
  required: ['output_path'],
73
83
  };
@@ -195,7 +205,7 @@ export function createDocumentTools(connection, snippetClient) {
195
205
  {
196
206
  tool: {
197
207
  name: 'ps_open_document',
198
- description: 'Open a file from disk into Photoshop with all dialogs suppressed (uses last-used Camera Raw settings for raw/HEIC). Returns document name, dimensions, color mode, and whether the source was a raw format. Use this in the pipeline to load Inbox files for editing.',
208
+ description: 'Open a file from disk into Photoshop with all dialogs suppressed (uses last-used Camera Raw settings for raw/HEIC). Returns document name, dimensions, color mode, and whether the source was a raw format. Use this in the pipeline to load Inbox files for editing. If the file is ALREADY open, its existing document is activated rather than opened a second time — `already_open: true` says so, and any edits made to it are still there (Photoshop would otherwise open a duplicate with a fresh Background, which silently strands prior work).',
199
209
  inputSchema: openDocumentSchema,
200
210
  outputSchema: {
201
211
  type: 'object',
@@ -208,6 +218,7 @@ export function createDocumentTools(connection, snippetClient) {
208
218
  color_mode: { type: 'string' },
209
219
  bits_per_channel: { type: 'number' },
210
220
  is_raw_source: { type: 'boolean' },
221
+ already_open: { type: 'boolean' },
211
222
  file_path: { type: 'string' },
212
223
  context: { type: 'object' },
213
224
  },
@@ -297,50 +308,34 @@ async function exportImage(connection, snippetClient, rawArgs) {
297
308
  }
298
309
  }
299
310
  async function createDocument(connection, snippetClient, rawArgs) {
300
- try {
301
- const args = validateArgs(createDocumentSchema, rawArgs);
302
- const width = args.width;
303
- const height = args.height;
304
- const resolution = args.resolution;
305
- const colorMode = args.color_mode;
306
- const colorModeMap = {
307
- RGB: 'NewDocumentMode.RGB',
308
- CMYK: 'NewDocumentMode.CMYK',
309
- Grayscale: 'NewDocumentMode.GRAYSCALE',
310
- };
311
- const mappedMode = colorModeMap[colorMode];
312
- if (!mappedMode) {
313
- throw new Error(`colorModeMap missing entry for "${colorMode}" — schema enum and ` +
314
- `internal mapping have drifted. Update src/tools/document-tools.ts.`);
315
- }
316
- const script = await snippetClient.build('newDocument', {
317
- width,
318
- height,
319
- resolution,
320
- colorMode: mappedMode,
321
- });
322
- const result = await runScript(connection, script);
323
- return {
324
- content: [
325
- {
326
- type: 'text',
327
- text: `Document created: ${width}x${height}px at ${resolution}dpi (${colorMode})`,
328
- },
329
- ],
330
- structuredContent: result,
331
- };
332
- }
333
- catch (error) {
334
- return {
335
- content: [
336
- {
337
- type: 'text',
338
- text: `Error creating document: ${error instanceof Error ? error.message : String(error)}`,
339
- },
340
- ],
341
- isError: true,
342
- };
343
- }
311
+ return runSnippetTool({
312
+ connection,
313
+ snippetClient,
314
+ rawArgs,
315
+ schema: createDocumentSchema,
316
+ snippet: 'newDocument',
317
+ errorPrefix: 'Error creating document',
318
+ params: (args) => {
319
+ const colorMode = args.color_mode;
320
+ const colorModeMap = {
321
+ RGB: 'NewDocumentMode.RGB',
322
+ CMYK: 'NewDocumentMode.CMYK',
323
+ Grayscale: 'NewDocumentMode.GRAYSCALE',
324
+ };
325
+ const mappedMode = colorModeMap[colorMode];
326
+ if (!mappedMode) {
327
+ throw new Error(`colorModeMap missing entry for "${colorMode}" — schema enum and ` +
328
+ `internal mapping have drifted. Update src/tools/document-tools.ts.`);
329
+ }
330
+ return {
331
+ width: args.width,
332
+ height: args.height,
333
+ resolution: args.resolution,
334
+ colorMode: mappedMode,
335
+ };
336
+ },
337
+ successText: (_result, args) => `Document created: ${args.width}x${args.height}px at ${args.resolution}dpi (${args.color_mode})`,
338
+ });
344
339
  }
345
340
  async function closeDocument(connection, snippetClient, rawArgs) {
346
341
  try {
@@ -364,24 +359,30 @@ async function closeDocument(connection, snippetClient, rawArgs) {
364
359
  };
365
360
  }
366
361
  catch (error) {
367
- return {
368
- content: [
369
- {
370
- type: 'text',
371
- text: `Error closing document: ${error instanceof Error ? error.message : String(error)}`,
372
- },
373
- ],
374
- isError: true,
375
- };
362
+ return toolErrorResult('Error closing document', error);
363
+ }
364
+ }
365
+ function isScriptTimeoutError(message) {
366
+ return /Script execution timeout|AppleEvent timed out|(?:^|[\s(])-1712(?:[\s)]|$)/i.test(message);
367
+ }
368
+ async function reprobeOpenDocument(connection, snippetClient, filePath) {
369
+ try {
370
+ const probeScript = await snippetClient.build('probeOpenDocument', { filePath });
371
+ const result = (await runScript(connection, probeScript, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS));
372
+ return result.success ? result : null;
373
+ }
374
+ catch {
375
+ return null;
376
376
  }
377
377
  }
378
378
  async function openDocumentPipeline(connection, snippetClient, rawArgs) {
379
+ let filePath;
379
380
  try {
380
381
  const args = validateArgs(openDocumentSchema, rawArgs);
381
- const filePath = args.file_path;
382
+ filePath = args.file_path;
382
383
  const suppressDialogs = args.suppress_dialogs;
383
384
  const script = await snippetClient.build('openDocumentPipeline', { filePath, suppressDialogs });
384
- const result = await runScript(connection, script);
385
+ const result = await runScript(connection, script, OPEN_DOCUMENT_TIMEOUT_MS);
385
386
  return {
386
387
  content: [
387
388
  {
@@ -393,45 +394,54 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
393
394
  };
394
395
  }
395
396
  catch (error) {
396
- return {
397
- content: [
398
- {
399
- type: 'text',
400
- text: `Error opening document: ${error instanceof Error ? error.message : String(error)}`,
401
- },
402
- ],
403
- isError: true,
404
- };
397
+ const message = error instanceof Error ? error.message : String(error);
398
+ if (filePath !== undefined && isScriptTimeoutError(message)) {
399
+ const reprobed = await reprobeOpenDocument(connection, snippetClient, filePath);
400
+ if (reprobed) {
401
+ return {
402
+ content: [
403
+ {
404
+ type: 'text',
405
+ text: `Document opened (the open exceeded the ${OPEN_DOCUMENT_TIMEOUT_MS}ms budget and was ` +
406
+ `reported as a timeout, but a post-timeout check confirmed it actually completed):\n` +
407
+ JSON.stringify(reprobed, null, 2),
408
+ },
409
+ ],
410
+ structuredContent: reprobed,
411
+ };
412
+ }
413
+ }
414
+ return toolErrorResult('Error opening document', error);
405
415
  }
406
416
  }
407
417
  async function savePsdPipeline(connection, snippetClient, rawArgs) {
408
- try {
409
- const args = validateArgs(savePsdSchema, rawArgs);
410
- const outputPath = args.output_path;
411
- const maximizeCompat = args.maximize_compatibility;
412
- const script = await snippetClient.build('savePsdAsCopy', { outputPath, maximizeCompat });
413
- const result = await runScript(connection, script);
414
- return {
415
- content: [
416
- {
417
- type: 'text',
418
- text: `PSD saved:\n${JSON.stringify(result, null, 2)}`,
419
- },
420
- ],
421
- structuredContent: result,
422
- };
418
+ let purged = 0;
419
+ if (rawArgs.keep_scene_channels !== true) {
420
+ try {
421
+ purged = await purgeSceneChannels(connection);
422
+ }
423
+ catch (err) {
424
+ logger.debug(`save_psd: scene-channel purge failed, saving anyway — ` +
425
+ `${err instanceof Error ? err.message : String(err)}`);
426
+ }
423
427
  }
424
- catch (error) {
425
- return {
426
- content: [
427
- {
428
- type: 'text',
429
- text: `Error saving PSD: ${error instanceof Error ? error.message : String(error)}`,
430
- },
431
- ],
432
- isError: true,
433
- };
428
+ const res = await runSnippetTool({
429
+ connection,
430
+ snippetClient,
431
+ rawArgs,
432
+ schema: savePsdSchema,
433
+ snippet: 'savePsdAsCopy',
434
+ errorPrefix: 'Error saving PSD',
435
+ params: (args) => ({
436
+ outputPath: args.output_path,
437
+ maximizeCompat: args.maximize_compatibility,
438
+ }),
439
+ successText: (result) => `PSD saved:\n${JSON.stringify(result, null, 2)}`,
440
+ });
441
+ if (!res.isError && typeof res.structuredContent === 'object' && res.structuredContent !== null) {
442
+ res.structuredContent.scene_channels_purged = purged;
434
443
  }
444
+ return res;
435
445
  }
436
446
  async function exportJpegPipeline(connection, snippetClient, rawArgs) {
437
447
  try {
@@ -459,48 +469,28 @@ async function exportJpegPipeline(connection, snippetClient, rawArgs) {
459
469
  };
460
470
  }
461
471
  catch (error) {
462
- return {
463
- content: [
464
- {
465
- type: 'text',
466
- text: `Error exporting JPEG: ${error instanceof Error ? error.message : String(error)}`,
467
- },
468
- ],
469
- isError: true,
470
- };
472
+ return toolErrorResult('Error exporting JPEG', error);
471
473
  }
472
474
  }
473
475
  async function exportPngPipeline(connection, snippetClient, rawArgs) {
474
- try {
475
- const args = validateArgs(exportPngSchema, rawArgs);
476
- const outputPath = args.output_path;
477
- const transparentBg = args.transparent_background;
478
- const longEdgePx = args.long_edge_px;
479
- const compression = args.compression;
480
- const params = { outputPath, transparentBg, compression };
481
- if (longEdgePx !== undefined)
482
- params.longEdgePx = longEdgePx;
483
- const script = await snippetClient.build('exportPngPipeline', params);
484
- const result = await runScript(connection, script);
485
- return {
486
- content: [
487
- {
488
- type: 'text',
489
- text: `PNG exported:\n${JSON.stringify(result, null, 2)}`,
490
- },
491
- ],
492
- structuredContent: result,
493
- };
494
- }
495
- catch (error) {
496
- return {
497
- content: [
498
- {
499
- type: 'text',
500
- text: `Error exporting PNG: ${error instanceof Error ? error.message : String(error)}`,
501
- },
502
- ],
503
- isError: true,
504
- };
505
- }
476
+ return runSnippetTool({
477
+ connection,
478
+ snippetClient,
479
+ rawArgs,
480
+ schema: exportPngSchema,
481
+ snippet: 'exportPngPipeline',
482
+ errorPrefix: 'Error exporting PNG',
483
+ params: (args) => {
484
+ const longEdgePx = args.long_edge_px;
485
+ const params = {
486
+ outputPath: args.output_path,
487
+ transparentBg: args.transparent_background,
488
+ compression: args.compression,
489
+ };
490
+ if (longEdgePx !== undefined)
491
+ params.longEdgePx = longEdgePx;
492
+ return params;
493
+ },
494
+ successText: (result) => `PNG exported:\n${JSON.stringify(result, null, 2)}`,
495
+ });
506
496
  }