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
@@ -3,14 +3,15 @@ import { promisify } from 'util';
3
3
  import { Logger } from '../utils/logger.js';
4
4
  import { TempDir } from '../utils/temp.js';
5
5
  import { runChildWithTimeout } from './run-child.js';
6
+ import { ScriptQueue } from './executor-queue.js';
6
7
  const execAsync = promisify(exec);
7
8
  export class MacOSExecutor {
8
9
  logger;
9
- scriptQueue = [];
10
- isProcessing = false;
10
+ queue;
11
11
  appName = 'Adobe Photoshop 2025';
12
12
  constructor() {
13
13
  this.logger = new Logger('MacOSExecutor');
14
+ this.queue = new ScriptQueue(this.logger);
14
15
  }
15
16
  setAppName(appName) {
16
17
  if (/["\\\r\n\t]/.test(appName)) {
@@ -20,91 +21,41 @@ export class MacOSExecutor {
20
21
  this.logger.debug(`App name set to: ${appName}`);
21
22
  }
22
23
  async execute(script, timeout = 30000) {
23
- return new Promise((resolve, reject) => {
24
- const task = {
25
- run: () => this.executeScript(script, timeout),
26
- resolve,
27
- reject,
28
- cancelled: false,
29
- settled: false,
30
- timeoutId: null,
31
- };
32
- const QUEUE_SLACK_MS = 3500;
33
- task.timeoutId = setTimeout(() => {
34
- if (task.settled)
35
- return;
36
- task.cancelled = true;
37
- task.settled = true;
38
- task.timeoutId = null;
39
- reject(new Error('Script execution timeout'));
40
- }, timeout + QUEUE_SLACK_MS);
41
- this.scriptQueue.push(task);
42
- void this.processQueue();
43
- });
44
- }
45
- settleTask(task, fn) {
46
- if (task.settled)
47
- return;
48
- task.settled = true;
49
- if (task.timeoutId !== null) {
50
- clearTimeout(task.timeoutId);
51
- task.timeoutId = null;
52
- }
53
- fn();
54
- }
55
- async processQueue() {
56
- if (this.isProcessing)
57
- return;
58
- this.isProcessing = true;
59
- try {
60
- while (this.scriptQueue.length > 0) {
61
- const task = this.scriptQueue.shift();
62
- if (task.cancelled)
63
- continue;
64
- try {
65
- const result = await task.run();
66
- this.settleTask(task, () => task.resolve(result));
67
- }
68
- catch (error) {
69
- if (task.settled) {
70
- this.logger.error('Script execution failed after settle:', error);
71
- }
72
- else {
73
- this.settleTask(task, () => task.reject(error));
74
- }
75
- }
76
- }
77
- }
78
- finally {
79
- this.isProcessing = false;
80
- }
24
+ return this.queue.enqueue(() => this.executeScript(script, timeout), timeout);
81
25
  }
82
26
  async executeScript(script, timeout) {
83
27
  const dir = await TempDir.create('editmamei-mac-');
84
28
  try {
85
29
  const jsxPath = await dir.write('script.jsx', script);
86
- const wrapperPath = await dir.write('wrapper.scpt', this.createAppleScriptWrapper(jsxPath));
87
- const { stdout, stderr } = await runChildWithTimeout('osascript', [wrapperPath], {
30
+ const wrapperPath = await dir.write('wrapper.scpt', this.createAppleScriptWrapper(jsxPath, timeout));
31
+ const { stdout, stderr, exitCode } = await runChildWithTimeout('osascript', [wrapperPath], {
88
32
  timeout,
89
33
  diagLabel: 'osascript wrapper.scpt',
90
34
  });
91
35
  if (stderr) {
92
36
  this.logger.warn('Script execution warning:', stderr);
93
37
  }
38
+ if (exitCode !== 0) {
39
+ throw new Error(`osascript exited with code ${exitCode}: ${stderr.trim() || '(no stderr)'}`);
40
+ }
94
41
  return this.parseResult(stdout);
95
42
  }
96
43
  finally {
97
44
  await dir.cleanup();
98
45
  }
99
46
  }
100
- createAppleScriptWrapper(jsxPath) {
47
+ createAppleScriptWrapper(jsxPath, timeoutMs = 30000) {
101
48
  const posixPath = jsxPath.replace(/\\/g, '/');
102
- if (posixPath.includes('"') || posixPath.includes("'") || posixPath.includes('\n')) {
49
+ if (posixPath.includes('"') || posixPath.includes('\n')) {
103
50
  throw new Error('jsxPath contains a character that would break AppleScript interpolation');
104
51
  }
105
- return `tell application "${this.appName}"
106
- \tdo javascript "$.evalFile(decodeURI('${encodeURI(posixPath)}'))"
107
- end tell`;
52
+ const timeoutSeconds = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1, Math.ceil(timeoutMs / 1000)) : 1;
53
+ const encodedPath = encodeURI(posixPath).replace(/'/g, '%27');
54
+ return `with timeout of ${timeoutSeconds} seconds
55
+ \ttell application "${this.appName}"
56
+ \t\tdo javascript "$.evalFile(decodeURI('${encodedPath}'))"
57
+ \tend tell
58
+ end timeout`;
108
59
  }
109
60
  parseResult(output) {
110
61
  const trimmed = output.trim();
@@ -87,9 +87,12 @@ export async function runChildWithTimeout(command, args, options) {
87
87
  });
88
88
  child.on('exit', (code, signal) => {
89
89
  if (timedOut) {
90
- settle(() => reject(new Error(`Script execution timeout after ${timeout}ms (${diagLabel}). The child process was killed; ` +
91
- `if Photoshop has a modal dialog open (license, missing font, GPU init, "Discard?" prompt), ` +
92
- `dismiss it in Photoshop and retry.`)));
90
+ settle(() => reject(new Error(`Script execution timeout after ${timeout}ms (${diagLabel}). The child process was killed, ` +
91
+ `but Photoshop runs as a separate process and may have kept executing — the operation ` +
92
+ `could still have completed. Check Photoshop's actual state before retrying. Common ` +
93
+ `causes: a genuinely slow operation (e.g. a large RAW file's first Camera Raw engine ` +
94
+ `init) exceeding the timeout, or a modal dialog open in Photoshop (license, missing ` +
95
+ `font, GPU init, "Discard?" prompt) — dismiss it if present.`)));
93
96
  return;
94
97
  }
95
98
  if (overflowed) {
@@ -26,11 +26,9 @@ export class WindowsDetector {
26
26
  this.logger.warn('Registry detection failed:', error);
27
27
  }
28
28
  const commonPaths = this.getCommonPaths();
29
- for (const path of commonPaths) {
30
- const info = await this.checkPath(path);
31
- if (info)
32
- return info;
33
- }
29
+ const commonPathInfo = await this.checkCommonPaths(commonPaths);
30
+ if (commonPathInfo)
31
+ return commonPathInfo;
34
32
  throw new Error('Photoshop not found on this system');
35
33
  }
36
34
  async detectFromRegistry() {
@@ -107,12 +105,21 @@ export class WindowsDetector {
107
105
  const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
108
106
  const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
109
107
  const paths = [];
110
- for (let year = 2025; year >= 2012; year--) {
108
+ const maxYear = Math.max(new Date().getFullYear() + 1, 2026);
109
+ for (let year = maxYear; year >= 2012; year--) {
111
110
  paths.push(`${programFiles}\\Adobe\\Adobe Photoshop ${year}\\Photoshop.exe`, `${programFilesX86}\\Adobe\\Adobe Photoshop ${year}\\Photoshop.exe`, `${programFiles}\\Adobe\\Adobe Photoshop CC ${year}\\Photoshop.exe`, `${programFilesX86}\\Adobe\\Adobe Photoshop CC ${year}\\Photoshop.exe`);
112
111
  }
113
112
  paths.push(`${programFiles}\\Adobe\\Adobe Photoshop CC\\Photoshop.exe`, `${programFiles}\\Adobe\\Photoshop CC\\Photoshop.exe`);
114
113
  return paths;
115
114
  }
115
+ async checkCommonPaths(paths) {
116
+ const results = await Promise.all(paths.map((path) => this.checkPath(path)));
117
+ const selected = results.find((result) => result !== null) ?? null;
118
+ if (selected) {
119
+ this.logger.info(`Found Photoshop at: ${selected.path}`);
120
+ }
121
+ return selected;
122
+ }
116
123
  async checkPath(path) {
117
124
  try {
118
125
  let cleanPath = path.trim().replace(/^"|"$/g, '');
@@ -121,7 +128,7 @@ export class WindowsDetector {
121
128
  }
122
129
  await access(cleanPath, constants.F_OK);
123
130
  const version = this.extractVersionFromPath(cleanPath);
124
- this.logger.info(`Found Photoshop at: ${cleanPath}`);
131
+ this.logger.debug(`Found Photoshop at: ${cleanPath}`);
125
132
  return {
126
133
  version,
127
134
  path: cleanPath,
@@ -4,73 +4,17 @@ import { exec } from 'child_process';
4
4
  import { Logger } from '../utils/logger.js';
5
5
  import { TempDir } from '../utils/temp.js';
6
6
  import { runChildWithTimeout } from './run-child.js';
7
+ import { ScriptQueue } from './executor-queue.js';
7
8
  const execAsync = promisify(exec);
8
9
  export class WindowsExecutor {
9
10
  logger;
10
- scriptQueue = [];
11
- isProcessing = false;
11
+ queue;
12
12
  constructor() {
13
13
  this.logger = new Logger('WindowsExecutor');
14
+ this.queue = new ScriptQueue(this.logger);
14
15
  }
15
16
  async execute(script, timeout = 30000) {
16
- return new Promise((resolve, reject) => {
17
- const task = {
18
- run: () => this.executeScript(script, timeout),
19
- resolve,
20
- reject,
21
- cancelled: false,
22
- settled: false,
23
- timeoutId: null,
24
- };
25
- const QUEUE_SLACK_MS = 3500;
26
- task.timeoutId = setTimeout(() => {
27
- if (task.settled)
28
- return;
29
- task.cancelled = true;
30
- task.settled = true;
31
- task.timeoutId = null;
32
- reject(new Error('Script execution timeout'));
33
- }, timeout + QUEUE_SLACK_MS);
34
- this.scriptQueue.push(task);
35
- void this.processQueue();
36
- });
37
- }
38
- settleTask(task, fn) {
39
- if (task.settled)
40
- return;
41
- task.settled = true;
42
- if (task.timeoutId !== null) {
43
- clearTimeout(task.timeoutId);
44
- task.timeoutId = null;
45
- }
46
- fn();
47
- }
48
- async processQueue() {
49
- if (this.isProcessing)
50
- return;
51
- this.isProcessing = true;
52
- try {
53
- while (this.scriptQueue.length > 0) {
54
- const task = this.scriptQueue.shift();
55
- if (task.cancelled)
56
- continue;
57
- try {
58
- const result = await task.run();
59
- this.settleTask(task, () => task.resolve(result));
60
- }
61
- catch (error) {
62
- if (task.settled) {
63
- this.logger.error('Script execution failed after settle:', error);
64
- }
65
- else {
66
- this.settleTask(task, () => task.reject(error));
67
- }
68
- }
69
- }
70
- }
71
- finally {
72
- this.isProcessing = false;
73
- }
17
+ return this.queue.enqueue(() => this.executeScript(script, timeout), timeout);
74
18
  }
75
19
  async executeScript(script, timeout) {
76
20
  const dir = await TempDir.create('editmamei-win-');
@@ -91,8 +35,8 @@ export class WindowsExecutor {
91
35
  }
92
36
  }
93
37
  createVBSWrapper(jsxPath) {
94
- if (jsxPath.includes('"') || jsxPath.includes("'")) {
95
- throw new Error('jsxPath contains a quote character — refusing to compose VBS');
38
+ if (jsxPath.includes('"')) {
39
+ throw new Error('jsxPath contains a double-quote — refusing to compose VBS');
96
40
  }
97
41
  return `
98
42
  On Error Resume Next
@@ -104,8 +48,9 @@ If Err.Number <> 0 Then
104
48
  WScript.Quit 1
105
49
  End If
106
50
 
107
- ' Execute the JSX script. The path is the mkdtemp temp file (quote-free by the
108
- ' guard above), but its single backslashes must be DOUBLED here: this string is
51
+ ' Execute the JSX script. The path is the mkdtemp temp file (double-quote-free
52
+ ' by the guard above; apostrophes are fine here), but its single backslashes
53
+ ' must be DOUBLED here: this string is
109
54
  ' handed to DoJavaScript as ExtendScript SOURCE, and a backslash inside a quoted
110
55
  ' path there is a JS string-literal escape (a Users/Temp segment would be parsed
111
56
  ' as an escape sequence and corrupt the path). The VBS Replace doubles each
Binary file
@@ -1,5 +1,6 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
+ import { toolErrorResult, runSnippetTool, applyToActiveLayerProp } from '../utils/tool-helpers.js';
3
4
  const addAdjustmentLayerSchema = {
4
5
  type: 'object',
5
6
  properties: {
@@ -495,6 +496,11 @@ const addAdjustmentLayerSchema = {
495
496
  description: 'Only meaningful when mask_from_selection is true AND there is an active selection. If true, the resulting mask is inverted — so the adjustment affects EVERYTHING OUTSIDE the selection rather than inside. Common idiom: "I selected the sky but want to adjust everything else." Defaults to false.',
496
497
  default: false,
497
498
  },
499
+ into_active_group: {
500
+ type: 'boolean',
501
+ description: "Photoshop's Mk-AdjL descriptor carries no placement target, so with a GROUP active it would natively nest the new layer INSIDE that group. Default false hoists the new layer back out so it lands above the active layer/group as a sibling, matching this tool's documented placement. Pass true to keep the new layer nested inside the active group instead.",
502
+ default: false,
503
+ },
498
504
  },
499
505
  required: ['type'],
500
506
  };
@@ -505,22 +511,14 @@ const colorLookupBakeSchema = {
505
511
  type: 'string',
506
512
  description: 'Leaf name of a 3DLUT file in PS\'s Presets/3DLUTs/ folder (e.g. "TealOrangePlusContrast.3DL", "FallColors.look", "Kodak 5205 Fuji 3510 (by Adobe).cube") OR an absolute path to a .3DL / .look / .cube file. Extension-agnostic leaf-name resolution (the same base name with a different extension still matches).',
507
513
  },
508
- apply_to_active_layer: {
509
- type: 'boolean',
510
- description: 'If false (default), runs on a DUPLICATE of the active layer; the original is preserved. If true, bakes into the active layer in place.',
511
- default: false,
512
- },
514
+ apply_to_active_layer: applyToActiveLayerProp('the color lookup'),
513
515
  },
514
516
  required: ['cl_lut_name'],
515
517
  };
516
518
  const equalizeSchema = {
517
519
  type: 'object',
518
520
  properties: {
519
- apply_to_active_layer: {
520
- type: 'boolean',
521
- description: 'If false (default), the operation runs on a DUPLICATE of the active layer and the original is preserved. If true, bakes into the active layer in place. Use false unless you specifically want the destructive-in-place behavior.',
522
- default: false,
523
- },
521
+ apply_to_active_layer: applyToActiveLayerProp('the equalize'),
524
522
  },
525
523
  };
526
524
  const shadowsHighlightsSchema = {
@@ -596,11 +594,7 @@ const shadowsHighlightsSchema = {
596
594
  maximum: 50,
597
595
  default: 0.01,
598
596
  },
599
- apply_to_active_layer: {
600
- type: 'boolean',
601
- description: 'If false (default), the op is applied to a duplicate of the active layer named "Shadows/Highlights (<Original Name>)" — original preserved. If true, bakes directly into the active layer.',
602
- default: false,
603
- },
597
+ apply_to_active_layer: applyToActiveLayerProp('the op'),
604
598
  },
605
599
  };
606
600
  const APPLY_ADJUSTMENT_TYPES = ['shadows_highlights', 'equalize', 'color_lookup'];
@@ -660,7 +654,7 @@ export function createAdjustmentTools(connection, snippetClient) {
660
654
  {
661
655
  tool: {
662
656
  name: 'ps_add_adjustment_layer',
663
- description: "Create a non-destructive adjustment layer above the active layer. Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for ALL tonal/color adjustments; the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `photoshop_merge_visible_layers`. Optionally clips the adjustment to only affect the layer directly below it. If a selection is active at call time, the new layer is automatically masked by it (toggle with mask_from_selection / mask_inverted). For destructive ops that don't have an adjustment-layer equivalent in Photoshop (Shadows/Highlights — single-pass shadow/highlight recovery), use `photoshop_apply_shadows_highlights` which auto-duplicates the active layer to keep the original intact. Returns context — the new adjustment layer becomes active.",
657
+ description: "Create a non-destructive adjustment layer above the active layer — hoisted out of the active layer's group by default even though Photoshop's own Mk-AdjL placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting). Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for ALL tonal/color adjustments; the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `ps_merge` (mode=visible). Optionally clips the adjustment to only affect the layer directly below it. If a selection is active at call time, the new layer is automatically masked by it (toggle with mask_from_selection / mask_inverted). For destructive ops that don't have an adjustment-layer equivalent in Photoshop (Shadows/Highlights — single-pass shadow/highlight recovery), use `ps_apply_adjustment` (type=shadows_highlights) which auto-duplicates the active layer to keep the original intact. Returns context (the new adjustment layer becomes active) plus parent_path — the actual containing-group chain, so placement is never silent.",
664
658
  inputSchema: addAdjustmentLayerSchema,
665
659
  outputSchema: {
666
660
  type: 'object',
@@ -677,6 +671,15 @@ export function createAdjustmentTools(connection, snippetClient) {
677
671
  mask_inverted: { type: 'boolean' },
678
672
  mask_inversion_error: { type: ['string', 'null'] },
679
673
  clipError: { type: 'string' },
674
+ hoisted: {
675
+ type: 'boolean',
676
+ description: 'True when the new layer had to be moved back out of the previously-active group to honor into_active_group:false (the default). False when it landed correctly on its own, or when the move-back itself failed — check the layer tree if this matters and hoisted is false.',
677
+ },
678
+ parent_path: {
679
+ type: ['array', 'null'],
680
+ items: { type: 'string' },
681
+ description: 'The containing-group name chain (outermost first), empty array at the document root.',
682
+ },
680
683
  context: { type: 'object' },
681
684
  },
682
685
  },
@@ -785,6 +788,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
785
788
  const name = args.name;
786
789
  const maskFromSelection = args.mask_from_selection ?? true;
787
790
  const maskInverted = args.mask_inverted ?? false;
791
+ const intoActiveGroup = args.into_active_group ?? false;
788
792
  const params = {};
789
793
  for (const key of FORWARDED_KEYS) {
790
794
  if (args[key] !== undefined)
@@ -806,6 +810,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
806
810
  clip_to_below: clipToBelow,
807
811
  mask_from_selection: maskFromSelection,
808
812
  mask_inverted: maskInverted,
813
+ into_active_group: intoActiveGroup,
809
814
  ...params,
810
815
  };
811
816
  if (name !== undefined)
@@ -823,125 +828,68 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
823
828
  };
824
829
  }
825
830
  catch (error) {
826
- return {
827
- content: [
828
- {
829
- type: 'text',
830
- text: `Error adding adjustment layer: ${error instanceof Error ? error.message : String(error)}`,
831
- },
832
- ],
833
- isError: true,
834
- };
831
+ return toolErrorResult('Error adding adjustment layer', error);
835
832
  }
836
833
  }
837
834
  async function applyColorLookup(connection, snippetClient, rawArgs) {
838
- try {
839
- const args = validateArgs(colorLookupBakeSchema, rawArgs);
840
- const lutName = args.cl_lut_name;
841
- const applyToActiveLayer = args.apply_to_active_layer ?? false;
842
- const script = await snippetClient.build('applyColorLookup', { lutName, applyToActiveLayer });
843
- const result = await runScript(connection, script);
844
- return {
845
- content: [
846
- {
847
- type: 'text',
848
- text: `Color Lookup applied (BAKE, experimental):\n${JSON.stringify(result, null, 2)}`,
849
- },
850
- ],
851
- structuredContent: result,
852
- };
853
- }
854
- catch (error) {
855
- return {
856
- content: [
857
- {
858
- type: 'text',
859
- text: `Error applying color lookup: ${error instanceof Error ? error.message : String(error)}`,
860
- },
861
- ],
862
- isError: true,
863
- };
864
- }
835
+ return runSnippetTool({
836
+ connection,
837
+ snippetClient,
838
+ rawArgs,
839
+ schema: colorLookupBakeSchema,
840
+ snippet: 'applyColorLookup',
841
+ errorPrefix: 'Error applying color lookup',
842
+ params: (args) => ({
843
+ lutName: args.cl_lut_name,
844
+ applyToActiveLayer: args.apply_to_active_layer ?? false,
845
+ }),
846
+ successText: (result) => `Color Lookup applied (BAKE, experimental):\n${JSON.stringify(result, null, 2)}`,
847
+ });
865
848
  }
866
849
  async function applyEqualize(connection, snippetClient, rawArgs) {
867
- try {
868
- const args = validateArgs(equalizeSchema, rawArgs);
869
- const applyToActiveLayer = args.apply_to_active_layer ?? false;
870
- const script = await snippetClient.build('applyEqualize', { applyToActiveLayer });
871
- const result = await runScript(connection, script);
872
- return {
873
- content: [
874
- {
875
- type: 'text',
876
- text: `Equalize applied:\n${JSON.stringify(result, null, 2)}`,
877
- },
878
- ],
879
- structuredContent: result,
880
- };
881
- }
882
- catch (error) {
883
- return {
884
- content: [
885
- {
886
- type: 'text',
887
- text: `Error applying equalize: ${error instanceof Error ? error.message : String(error)}`,
888
- },
889
- ],
890
- isError: true,
891
- };
892
- }
850
+ return runSnippetTool({
851
+ connection,
852
+ snippetClient,
853
+ rawArgs,
854
+ schema: equalizeSchema,
855
+ snippet: 'applyEqualize',
856
+ errorPrefix: 'Error applying equalize',
857
+ params: (args) => ({
858
+ applyToActiveLayer: args.apply_to_active_layer ?? false,
859
+ }),
860
+ successText: (result) => `Equalize applied:\n${JSON.stringify(result, null, 2)}`,
861
+ });
893
862
  }
894
863
  async function applyShadowsHighlights(connection, snippetClient, rawArgs) {
895
- try {
896
- const args = validateArgs(shadowsHighlightsSchema, rawArgs);
897
- const shadowAmount = args.shadow_amount ?? 35;
898
- const shadowWidth = args.shadow_width ?? 50;
899
- const shadowRadius = args.shadow_radius ?? 30;
900
- const highlightAmount = args.highlight_amount ?? 0;
901
- const highlightWidth = args.highlight_width ?? 50;
902
- const highlightRadius = args.highlight_radius ?? 30;
903
- const colorCorrection = args.color_correction ?? 20;
904
- const midtoneContrast = args.midtone_contrast ?? 0;
905
- const blackClip = args.black_clip ?? 0.01;
906
- const whiteClip = args.white_clip ?? 0.01;
907
- const applyToActiveLayer = args.apply_to_active_layer ?? false;
908
- const script = await snippetClient.build('applyShadowsHighlights', {
909
- shadowAmount,
910
- shadowWidth,
911
- shadowRadius,
912
- highlightAmount,
913
- highlightWidth,
914
- highlightRadius,
915
- colorCorrection,
916
- midtoneContrast,
917
- blackClip,
918
- whiteClip,
919
- applyToActiveLayer,
920
- });
921
- const result = await runScript(connection, script);
922
- const r = result;
923
- const target = r.target_was_copy
924
- ? `new copy "${r.target_layer_name ?? '?'}"`
925
- : 'active layer (in place)';
926
- return {
927
- content: [
928
- {
929
- type: 'text',
930
- text: `Shadows/Highlights applied to ${target} (shadow ${shadowAmount}, highlight ${highlightAmount}, color ${colorCorrection}).`,
931
- },
932
- ],
933
- structuredContent: result,
934
- };
935
- }
936
- catch (error) {
937
- return {
938
- content: [
939
- {
940
- type: 'text',
941
- text: `Error applying shadows/highlights: ${error instanceof Error ? error.message : String(error)}`,
942
- },
943
- ],
944
- isError: true,
945
- };
946
- }
864
+ return runSnippetTool({
865
+ connection,
866
+ snippetClient,
867
+ rawArgs,
868
+ schema: shadowsHighlightsSchema,
869
+ snippet: 'applyShadowsHighlights',
870
+ errorPrefix: 'Error applying shadows/highlights',
871
+ params: (args) => ({
872
+ shadowAmount: args.shadow_amount ?? 35,
873
+ shadowWidth: args.shadow_width ?? 50,
874
+ shadowRadius: args.shadow_radius ?? 30,
875
+ highlightAmount: args.highlight_amount ?? 0,
876
+ highlightWidth: args.highlight_width ?? 50,
877
+ highlightRadius: args.highlight_radius ?? 30,
878
+ colorCorrection: args.color_correction ?? 20,
879
+ midtoneContrast: args.midtone_contrast ?? 0,
880
+ blackClip: args.black_clip ?? 0.01,
881
+ whiteClip: args.white_clip ?? 0.01,
882
+ applyToActiveLayer: args.apply_to_active_layer ?? false,
883
+ }),
884
+ successText: (result, args) => {
885
+ const r = result;
886
+ const target = r.target_was_copy
887
+ ? `new copy "${r.target_layer_name ?? '?'}"`
888
+ : 'active layer (in place)';
889
+ const shadowAmount = args.shadow_amount ?? 35;
890
+ const highlightAmount = args.highlight_amount ?? 0;
891
+ const colorCorrection = args.color_correction ?? 20;
892
+ return `Shadows/Highlights applied to ${target} (shadow ${shadowAmount}, highlight ${highlightAmount}, color ${colorCorrection}).`;
893
+ },
894
+ });
947
895
  }
@@ -1,6 +1,7 @@
1
1
  import { SUPPORTED_BRUSH_TOOLS } from '../api/brush-tool-names.js';
2
2
  import { runScript } from '../utils/run-script.js';
3
3
  import { validateArgs } from '../utils/validate.js';
4
+ import { toolErrorResult, applyToActiveLayerProp } from '../utils/tool-helpers.js';
4
5
  import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
5
6
  import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
6
7
  const TOOLS_REQUIRING_SOURCE = ['clone_stamp', 'healing_brush'];
@@ -109,11 +110,7 @@ const applyBrushStrokeSchema = {
109
110
  maximum: 50,
110
111
  default: 0,
111
112
  },
112
- apply_to_active_layer: {
113
- type: 'boolean',
114
- description: 'If false (default), the stroke runs on a duplicate named "Brush Stroke (<original name>)" — the original is preserved, undo is delete-the-copy (Bundle O pattern). If true, the stroke bakes into the active layer directly.',
115
- default: false,
116
- },
113
+ apply_to_active_layer: applyToActiveLayerProp('the stroke'),
117
114
  },
118
115
  required: ['tool', 'brush_size'],
119
116
  };
@@ -292,15 +289,7 @@ async function applyBrushStroke(connection, snippetClient, client, rawArgs) {
292
289
  };
293
290
  }
294
291
  catch (error) {
295
- return {
296
- content: [
297
- {
298
- type: 'text',
299
- text: `Error applying brush stroke: ${error instanceof Error ? error.message : String(error)}`,
300
- },
301
- ],
302
- isError: true,
303
- };
292
+ return toolErrorResult('Error applying brush stroke', error);
304
293
  }
305
294
  }
306
295
  function deterministicNoise(i, anchorSeed, axisSalt) {