editmamei 0.19.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/NOTICES.md +17 -1
  2. package/README.md +9 -8
  3. package/dist/api/extendscript/_helpers.js +1 -1
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/config.js +3 -3
  8. package/dist/cli/help.js +4 -0
  9. package/dist/cli/install.js +2 -2
  10. package/dist/cli/report.js +11 -0
  11. package/dist/cli/router.js +20 -0
  12. package/dist/core/server.js +8 -8
  13. package/dist/core/settings.js +2 -2
  14. package/dist/core/tool-groups.js +92 -82
  15. package/dist/core/tool-tiers.js +91 -81
  16. package/dist/detection/landmark-detection-client.js +8 -0
  17. package/dist/detection/mesh-eye-correction.js +126 -0
  18. package/dist/detection/sam-segmenter.js +83 -0
  19. package/dist/diagnostics/collect.js +169 -0
  20. package/dist/index.js +2 -0
  21. package/dist/license/entitlement.js +52 -0
  22. package/dist/modules/ce/index.js +6 -4
  23. package/dist/perception/grounding-anchors.js +37 -0
  24. package/dist/perception/grounding-corners.js +93 -0
  25. package/dist/perception/grounding-edge-trace.js +90 -0
  26. package/dist/perception/grounding-extrema.js +41 -0
  27. package/dist/perception/grounding-gate.js +154 -0
  28. package/dist/perception/grounding-geometry.js +132 -0
  29. package/dist/perception/grounding-grid.js +34 -0
  30. package/dist/perception/grounding-landmarks.js +40 -0
  31. package/dist/perception/grounding-locate.js +341 -0
  32. package/dist/perception/grounding-resolver.js +202 -0
  33. package/dist/perception/grounding-review-crop.js +85 -0
  34. package/dist/perception/grounding-warp.js +182 -0
  35. package/dist/perception/sky-mask-transfer.js +50 -18
  36. package/dist/skills/editmamei-skill.zip +0 -0
  37. package/dist/tools/adjustment-tools.js +3 -3
  38. package/dist/tools/brush-tools.js +111 -23
  39. package/dist/tools/channel-compose-tools.js +241 -0
  40. package/dist/tools/detection-tools.js +1 -1
  41. package/dist/tools/diagnostics-tools.js +66 -0
  42. package/dist/tools/document-tools.js +6 -6
  43. package/dist/tools/filter-tools.js +476 -19
  44. package/dist/tools/group-tools.js +10 -10
  45. package/dist/tools/guide-tools.js +4 -4
  46. package/dist/tools/history-tools.js +4 -4
  47. package/dist/tools/image-placement-tools.js +1 -1
  48. package/dist/tools/image-tools.js +62 -19
  49. package/dist/tools/inspect-tools.js +2 -2
  50. package/dist/tools/layer-ordering-tools.js +1 -1
  51. package/dist/tools/layer-properties-tools.js +42 -23
  52. package/dist/tools/layer-tools.js +10 -10
  53. package/dist/tools/layer-transform-tools.js +80 -41
  54. package/dist/tools/overview-tools.js +42 -42
  55. package/dist/tools/path-tools.js +53 -10
  56. package/dist/tools/portrait-tools.js +3 -3
  57. package/dist/tools/preview-tools.js +7 -82
  58. package/dist/tools/retouch-tools.js +93 -25
  59. package/dist/tools/scene-tools.js +13 -13
  60. package/dist/tools/selection-tools.js +319 -40
  61. package/dist/tools/shape-tools.js +216 -0
  62. package/dist/tools/text-tools.js +1 -1
  63. package/dist/tools/transform-canvas-tools.js +2 -2
  64. package/dist/tools/vector-mask-tools.js +23 -8
  65. package/dist/utils/log-buffer.js +41 -0
  66. package/dist/utils/logger.js +12 -1
  67. package/dist/utils/session-log-reader.js +27 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +1 -1
  70. package/dist/preview/coordinate-grid.js +0 -250
  71. package/dist/tools/object-tools.js +0 -193
  72. package/dist/tools/text-on-object-tools.js +0 -225
@@ -77,7 +77,7 @@ export function createGroupTools(connection, snippetClient) {
77
77
  return [
78
78
  {
79
79
  tool: {
80
- name: 'photoshop_create_group',
80
+ name: 'ps_create_group',
81
81
  description: 'Create a new layer group (LayerSet) above the active layer with the given name. Optionally moves existing layers into it in one step. Non-destructive. Foundational for structured non-destructive editing — e.g. group all adjustment layers into an "edits" group so you can A/B toggle the whole stack via group visibility.',
82
82
  inputSchema: createGroupSchema,
83
83
  outputSchema: {
@@ -99,7 +99,7 @@ export function createGroupTools(connection, snippetClient) {
99
99
  },
100
100
  {
101
101
  tool: {
102
- name: 'photoshop_move_layer_to_group',
102
+ name: 'ps_move_layer_to_group',
103
103
  description: 'Move a named layer into a named group. The layer is placed at the top of the group stack. Both layer_name and group_name are looked up recursively. Throws if either is not found, or if the layer IS the group itself.',
104
104
  inputSchema: moveLayerToGroupSchema,
105
105
  outputSchema: {
@@ -120,7 +120,7 @@ export function createGroupTools(connection, snippetClient) {
120
120
  },
121
121
  {
122
122
  tool: {
123
- name: 'photoshop_set_group_blend_mode',
123
+ name: 'ps_set_group_blend_mode',
124
124
  description: "Set a group's blend mode. The default for new groups is PASSTHROUGH (adjustments inside affect layers below the group). Change to NORMAL when treating the group as a single composite — required for masking a stack of adjustments with one mask.",
125
125
  inputSchema: setGroupBlendModeSchema,
126
126
  outputSchema: {
@@ -141,7 +141,7 @@ export function createGroupTools(connection, snippetClient) {
141
141
  },
142
142
  {
143
143
  tool: {
144
- name: 'photoshop_ungroup',
144
+ name: 'ps_ungroup',
145
145
  description: 'DESTRUCTIVE structural change: Dissolve a group, leaving its contents at the parent level in their existing stack order. The group itself is removed. Requires confirm:true. Recoverable only via Edit > Undo.',
146
146
  inputSchema: ungroupSchema,
147
147
  outputSchema: {
@@ -164,8 +164,8 @@ export function createGroupTools(connection, snippetClient) {
164
164
  },
165
165
  {
166
166
  tool: {
167
- name: 'photoshop_create_clipping_mask',
168
- description: 'Clip the active layer to the layer directly below it (uses that layer as the alpha source). Non-destructive — the upper layer is unchanged; PS just paints it only where the layer below has pixels. Common for masking adjustment-layer effect to a single underlying layer, or constraining a texture/photo to a shape. Equivalent to the PS menu Layer > Create Clipping Mask (Ctrl+Alt+G). Reverse it with the PS menu Layer > Release Clipping Mask, or photoshop_undo. This is the standalone primitive — the add_adjustment_layer tool already accepts clip_to_below for the adjustment-layer-specific case.',
167
+ name: 'ps_create_clipping_mask',
168
+ description: 'Clip the active layer to the layer directly below it (uses that layer as the alpha source). Non-destructive — the upper layer is unchanged; PS just paints it only where the layer below has pixels. Common for masking adjustment-layer effect to a single underlying layer, or constraining a texture/photo to a shape. Equivalent to the PS menu Layer > Create Clipping Mask (Ctrl+Alt+G). Reverse it with the PS menu Layer > Release Clipping Mask, or ps_undo. This is the standalone primitive — the add_adjustment_layer tool already accepts clip_to_below for the adjustment-layer-specific case.',
169
169
  inputSchema: { type: 'object', properties: {}, required: [] },
170
170
  outputSchema: {
171
171
  type: 'object',
@@ -184,8 +184,8 @@ export function createGroupTools(connection, snippetClient) {
184
184
  },
185
185
  {
186
186
  tool: {
187
- name: 'photoshop_release_clipping_mask',
188
- description: "Release the active layer's clipping mask (the inverse of photoshop_create_clipping_mask). The layer returns to compositing against the whole canvas instead of just the layer below. Equivalent to PS menu Layer > Release Clipping Mask. Idempotent on a non-clipped layer (PS silently no-ops).",
187
+ name: 'ps_release_clipping_mask',
188
+ description: "Release the active layer's clipping mask (the inverse of ps_create_clipping_mask). The layer returns to compositing against the whole canvas instead of just the layer below. Equivalent to PS menu Layer > Release Clipping Mask. Idempotent on a non-clipped layer (PS silently no-ops).",
189
189
  inputSchema: { type: 'object', properties: {}, required: [] },
190
190
  outputSchema: {
191
191
  type: 'object',
@@ -204,8 +204,8 @@ export function createGroupTools(connection, snippetClient) {
204
204
  },
205
205
  {
206
206
  tool: {
207
- name: 'photoshop_delete_group',
208
- description: 'DESTRUCTIVE: Delete a group AND ALL its contents (including nested groups and their layers). Requires confirm:true because the destructive scope is much larger than a single-layer delete. Recoverable only via Edit > Undo. To dissolve a group while keeping its contents, use photoshop_ungroup instead.',
207
+ name: 'ps_delete_group',
208
+ description: 'DESTRUCTIVE: Delete a group AND ALL its contents (including nested groups and their layers). Requires confirm:true because the destructive scope is much larger than a single-layer delete. Recoverable only via Edit > Undo. To dissolve a group while keeping its contents, use ps_ungroup instead.',
209
209
  inputSchema: deleteGroupSchema,
210
210
  outputSchema: {
211
211
  type: 'object',
@@ -10,7 +10,7 @@ const addGuideSchema = {
10
10
  },
11
11
  position: {
12
12
  type: 'number',
13
- description: 'Guide position in document pixels (the x for vertical, the y for horizontal).',
13
+ description: 'Guide position in document pixels (the x for a vertical guide, the y for a horizontal guide).',
14
14
  minimum: 0,
15
15
  },
16
16
  },
@@ -51,8 +51,8 @@ export function createGuideTools(connection, snippetClient) {
51
51
  return [
52
52
  {
53
53
  tool: {
54
- name: 'photoshop_guides',
55
- description: 'Manage document guides — choose with `op`. `add` places one guide (orientation horizontal|vertical + position in px). `layout` creates an evenly-spaced grid (columns and/or rows — great for rule-of-thirds at 3×3). `clear` removes all guides. Guides are non-printing layout aids.',
54
+ name: 'ps_guides',
55
+ description: 'Manage document guides — non-printing layout aids for a HUMAN working in Photoshop. Guides are editor-only chrome: they do NOT render into ps_get_preview (the exported/flattened image), so they are a user authoring aid, not a model-perception tool — place them when a person will open the PSD and wants alignment/composition marks. Choose with `op`. `add` places one guide (orientation horizontal|vertical) at a `position` in px. `layout` creates an evenly-spaced grid (columns and/or rows — great for rule-of-thirds at 3×3). `clear` removes all guides.',
56
56
  inputSchema: GUIDE_INPUT_SCHEMA,
57
57
  outputSchema: {
58
58
  type: 'object',
@@ -103,7 +103,7 @@ async function addGuide(connection, snippetClient, rawArgs) {
103
103
  const orientation = args.orientation;
104
104
  const position = args.position;
105
105
  const script = await snippetClient.build('addGuide', { orientation, position });
106
- const result = await runScript(connection, script);
106
+ const result = (await runScript(connection, script));
107
107
  return {
108
108
  content: [
109
109
  {
@@ -38,8 +38,8 @@ export function createHistoryTools(connection, snippetClient) {
38
38
  return [
39
39
  {
40
40
  tool: {
41
- name: 'photoshop_undo',
42
- description: 'Step backward in the document history (equivalent to Ctrl/Cmd+Z). WHEN TO REACH FOR THIS: after an experimental destructive bake (photoshop_merge_visible_layers, photoshop_flatten_image, photoshop_apply_*_blur, photoshop_apply_sharpen, photoshop_apply_noise) produced a wrong result; after a play_action whose scope you mispredicted; or to revert an entire branch of exploration. Non-destructive workflows (adjustment layers + masks) rarely need this — just tweak/delete the offending layer instead. Reversible via photoshop_redo as long as no new edit has been made since. Returns current history state, remaining steps, and document context.',
41
+ name: 'ps_undo',
42
+ description: 'Step backward in the document history (equivalent to Ctrl/Cmd+Z). WHEN TO REACH FOR THIS: after an experimental destructive bake (photoshop_merge_visible_layers, photoshop_flatten_image, photoshop_apply_*_blur, photoshop_apply_sharpen, photoshop_apply_noise) produced a wrong result; after a play_action whose scope you mispredicted; or to revert an entire branch of exploration. Non-destructive workflows (adjustment layers + masks) rarely need this — just tweak/delete the offending layer instead. Reversible via ps_redo as long as no new edit has been made since. Returns current history state, remaining steps, and document context.',
43
43
  inputSchema: undoSchema,
44
44
  outputSchema: HISTORY_STEP_RESULT_SCHEMA,
45
45
  annotations: {
@@ -51,8 +51,8 @@ export function createHistoryTools(connection, snippetClient) {
51
51
  },
52
52
  {
53
53
  tool: {
54
- name: 'photoshop_redo',
55
- description: 'Step forward in the document history (equivalent to Ctrl/Cmd+Shift+Z). Only works if photoshop_undo was the last operation; making a fresh edit destroys the redo stack. Returns updated context.',
54
+ name: 'ps_redo',
55
+ description: 'Step forward in the document history (equivalent to Ctrl/Cmd+Shift+Z). Only works if ps_undo was the last operation; making a fresh edit destroys the redo stack. Returns updated context.',
56
56
  inputSchema: redoSchema,
57
57
  outputSchema: HISTORY_STEP_RESULT_SCHEMA,
58
58
  annotations: {
@@ -41,7 +41,7 @@ export function createImagePlacementTools(connection, snippetClient) {
41
41
  return [
42
42
  {
43
43
  tool: {
44
- name: 'photoshop_place_image',
44
+ name: 'ps_place_image',
45
45
  description: 'Place an image file (JPEG, PNG, PSD, etc.) as a new Smart Object layer in the active document, optionally offset from center. Rasterize separately if you need to apply pixel-only operations. Open-world: reads from the filesystem. Returns the new layer name, bounds, and updated document context.',
46
46
  inputSchema: placeImageSchema,
47
47
  outputSchema: {
@@ -1,5 +1,7 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
+ import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
4
+ import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
3
5
  const PS_MAX_DIM_PX = 300_000;
4
6
  const resizeImageSchema = {
5
7
  type: 'object',
@@ -22,32 +24,37 @@ const resizeImageSchema = {
22
24
  const cropDocumentSchema = {
23
25
  type: 'object',
24
26
  properties: {
27
+ placement: {
28
+ ...PLACEMENT_SCHEMA,
29
+ description: 'ANCHOR-RELATIONAL crop (preferred over guessing pixels): a REGION relation (inside/gap) → the crop is the ' +
30
+ 'resolved region bounding box, verified by the gate. Crops ONLY if the gate PASSES. When set, ' +
31
+ 'left/top/right/bottom are ignored. See ps_resolve_placement for the anchors + relation vocabulary.',
32
+ },
25
33
  left: {
26
34
  type: 'integer',
27
- description: 'Left edge position in pixels.',
35
+ description: 'Left edge position in pixels (raw mode; ignored when placement is set).',
28
36
  minimum: 0,
29
37
  maximum: PS_MAX_DIM_PX,
30
38
  },
31
39
  top: {
32
40
  type: 'integer',
33
- description: 'Top edge position in pixels.',
41
+ description: 'Top edge position in pixels (raw mode; ignored when placement is set).',
34
42
  minimum: 0,
35
43
  maximum: PS_MAX_DIM_PX,
36
44
  },
37
45
  right: {
38
46
  type: 'integer',
39
- description: 'Right edge position in pixels.',
47
+ description: 'Right edge position in pixels (raw mode; ignored when placement is set).',
40
48
  minimum: 1,
41
49
  maximum: PS_MAX_DIM_PX,
42
50
  },
43
51
  bottom: {
44
52
  type: 'integer',
45
- description: 'Bottom edge position in pixels.',
53
+ description: 'Bottom edge position in pixels (raw mode; ignored when placement is set).',
46
54
  minimum: 1,
47
55
  maximum: PS_MAX_DIM_PX,
48
56
  },
49
57
  },
50
- required: ['left', 'top', 'right', 'bottom'],
51
58
  };
52
59
  const convertImageModeSchema = {
53
60
  type: 'object',
@@ -80,11 +87,11 @@ const convertImageModeSchema = {
80
87
  },
81
88
  required: ['mode'],
82
89
  };
83
- export function createImageTools(connection, snippetClient) {
90
+ export function createImageTools(connection, snippetClient, client = new OnnxLandmarkDetectionClient()) {
84
91
  return [
85
92
  {
86
93
  tool: {
87
- name: 'photoshop_resize_image',
94
+ name: 'ps_resize_image',
88
95
  description: 'Resize the entire document canvas (all layers) to the specified absolute dimensions. Destructive: downscaling permanently loses pixel data. Idempotent: same width/height always yield the same result. For aspect-preserving downscale during export, see photoshop_export_jpeg / photoshop_export_png which resize a duplicate. Returns updated document context.',
89
96
  inputSchema: resizeImageSchema,
90
97
  outputSchema: {
@@ -107,8 +114,8 @@ export function createImageTools(connection, snippetClient) {
107
114
  },
108
115
  {
109
116
  tool: {
110
- name: 'photoshop_crop_document',
111
- description: 'Crop the document to the specified absolute pixel bounds. Destructive: pixels outside the bounds are permanently lost. Idempotent: same bounds always yield the same result. Coordinates are in document space. Returns updated document context.',
117
+ name: 'ps_crop_document',
118
+ description: 'Crop the document to a region — EITHER anchor-relational `placement` (preferred: a region relation like inside/gap → the crop is the resolved, gate-verified region bbox, no pixel-guessing) OR absolute pixel bounds (left/top/right/bottom in document space). Destructive: pixels outside the bounds are permanently lost. Idempotent for the same bounds. Returns updated document context.',
112
119
  inputSchema: cropDocumentSchema,
113
120
  outputSchema: {
114
121
  type: 'object',
@@ -116,6 +123,16 @@ export function createImageTools(connection, snippetClient) {
116
123
  cropped: { type: 'boolean' },
117
124
  newWidth: { type: 'number' },
118
125
  newHeight: { type: 'number' },
126
+ placement: {
127
+ type: 'object',
128
+ description: 'Present when anchor-relational placement was used: the resolved region + gate verdict.',
129
+ properties: {
130
+ target: { type: 'string' },
131
+ gate: { type: 'object' },
132
+ anchors: { type: 'object' },
133
+ geometry: { type: 'object' },
134
+ },
135
+ },
119
136
  context: { type: 'object' },
120
137
  },
121
138
  },
@@ -125,11 +142,11 @@ export function createImageTools(connection, snippetClient) {
125
142
  idempotentHint: true,
126
143
  },
127
144
  },
128
- handler: async (args) => cropDocument(connection, snippetClient, args),
145
+ handler: async (args) => cropDocument(connection, snippetClient, client, args),
129
146
  },
130
147
  {
131
148
  tool: {
132
- name: 'photoshop_convert_image_mode',
149
+ name: 'ps_convert_image_mode',
133
150
  description: 'Convert the document color mode (grayscale / rgb / cmyk / lab). Destructive and document-wide: the image is FLATTENED and, for grayscale, color is discarded (the confirmation prompt is auto-accepted). Use grayscale as the base for halftone / line-art workflows, or rgb to bring a CMYK/Lab doc back for normal editing. Returns updated document context.',
134
151
  inputSchema: convertImageModeSchema,
135
152
  outputSchema: {
@@ -218,23 +235,49 @@ async function convertImageMode(connection, snippetClient, rawArgs) {
218
235
  };
219
236
  }
220
237
  }
221
- async function cropDocument(connection, snippetClient, rawArgs) {
238
+ async function cropDocument(connection, snippetClient, client, rawArgs) {
222
239
  try {
223
240
  const args = validateArgs(cropDocumentSchema, rawArgs);
224
- const left = args.left;
225
- const top = args.top;
226
- const right = args.right;
227
- const bottom = args.bottom;
241
+ let left, top, right, bottom;
242
+ let placementInfo;
243
+ if (args.placement) {
244
+ const rp = await resolveExpectedPlacement(connection, client, args.placement, 'region', 'crop');
245
+ ({ left, top, right, bottom } = rp.bbox);
246
+ placementInfo = { target: rp.target, anchors: rp.anchors, geometry: rp.bbox };
247
+ }
248
+ else {
249
+ for (const k of ['left', 'top', 'right', 'bottom']) {
250
+ if (typeof args[k] !== 'number') {
251
+ throw new Error(`crop needs numeric left/top/right/bottom (missing ${k}), or a placement (anchors + a region relation).`);
252
+ }
253
+ }
254
+ left = args.left;
255
+ top = args.top;
256
+ right = args.right;
257
+ bottom = args.bottom;
258
+ }
228
259
  const script = await snippetClient.build('cropDocument', { left, top, right, bottom });
229
- const result = await runScript(connection, script);
260
+ const result = (await runScript(connection, script));
230
261
  return {
231
262
  content: [
232
263
  {
233
264
  type: 'text',
234
- text: `Document cropped\nResult: ${JSON.stringify(result)}`,
265
+ text: placementInfo
266
+ ? `Document cropped to [${left},${top},${right},${bottom}] (placement gate PASS)\nResult: ${JSON.stringify(result)}`
267
+ : `Document cropped\nResult: ${JSON.stringify(result)}`,
235
268
  },
236
269
  ],
237
- structuredContent: result,
270
+ structuredContent: placementInfo
271
+ ? {
272
+ ...result,
273
+ placement: {
274
+ target: placementInfo.target,
275
+ gate: { pass: true },
276
+ anchors: placementInfo.anchors,
277
+ geometry: placementInfo.geometry,
278
+ },
279
+ }
280
+ : result,
238
281
  };
239
282
  }
240
283
  catch (error) {
@@ -23,8 +23,8 @@ export function createInspectTools(connection, snippetClient) {
23
23
  return [
24
24
  {
25
25
  tool: {
26
- name: 'photoshop_inspect',
27
- description: 'Read-only document inspection — choose with `what` (metadata / layer_tree / history / selection_info). This is the assess/orientation surface: call it at the start of a workflow and whenever you need fresh state. For metadata, pass `sections` to subset (e.g. ["context"] for a cheap probe). For an IMAGE-based check use photoshop_get_preview; for NUMERIC verification use photoshop_get_histogram / photoshop_compare_regions / photoshop_get_layer_bounds_diff (these stay separate, named tools on purpose). Read-only and idempotent.',
26
+ name: 'ps_inspect',
27
+ description: 'Read-only document inspection — choose with `what` (metadata / layer_tree / history / selection_info). This is the assess/orientation surface: call it at the start of a workflow and whenever you need fresh state. For metadata, pass `sections` to subset (e.g. ["context"] for a cheap probe). For an IMAGE-based check use ps_get_preview; for NUMERIC verification use ps_get_histogram / ps_compare_regions / ps_get_layer_bounds_diff (these stay separate, named tools on purpose). Read-only and idempotent.',
28
28
  inputSchema: INSPECT_INPUT_SCHEMA,
29
29
  outputSchema: {
30
30
  type: 'object',
@@ -35,7 +35,7 @@ export function createLayerOrderingTools(connection, snippetClient) {
35
35
  return [
36
36
  {
37
37
  tool: {
38
- name: 'photoshop_move_layer_to_position',
38
+ name: 'ps_move_layer_to_position',
39
39
  description: 'Move a layer to a position in the stack — the single ordering primitive (the per-direction helpers `_to_top` / `_to_bottom` / `_up` / `_down` were removed 2026-05-31; this tool covers all four cases). Position keywords: ABOVE/BELOW (requires target_layer_name — places the moved layer immediately above/below the target), TOP/BOTTOM (absolute — target_layer_name not needed). By default the ACTIVE layer is moved; pass layer_to_move=<name> to move a specific layer by name (recurses into groups). COMMON CASES: an adjustment layer just created landed below another adjustment that masks its effect → move it to TOP; a vignette darkens the wrong layers → move ABOVE the layer it should affect; a sky-replacement composite is showing through the foreground → move the foreground to TOP. Idempotent for a given (layer_to_move, target, position).',
40
40
  inputSchema: moveLayerToPositionSchema,
41
41
  outputSchema: LAYER_MOVE_RESULT_SCHEMA,
@@ -5,6 +5,18 @@ const emptySchema = {
5
5
  type: 'object',
6
6
  properties: {},
7
7
  };
8
+ const convertToSmartObjectSchema = {
9
+ type: 'object',
10
+ properties: {
11
+ mode: {
12
+ type: 'string',
13
+ enum: ['convert', 'new_via_copy'],
14
+ description: 'convert (default): wrap the active layer into a Smart Object in place. ' +
15
+ "new_via_copy: the active layer must ALREADY be a Smart Object — make a NEW Smart Object that is an INDEPENDENT copy (its own embedded source), unlinked from the original's shared source, so editing the copy's contents does not propagate back. (Differs from ps_duplicate_layer of a Smart Object, which keeps the shared source.)",
16
+ default: 'convert',
17
+ },
18
+ },
19
+ };
8
20
  const setLayerOpacitySchema = {
9
21
  type: 'object',
10
22
  properties: {
@@ -77,8 +89,8 @@ const addLayerStyleSchema = {
77
89
  properties: {
78
90
  style: {
79
91
  type: 'string',
80
- enum: ['drop_shadow', 'stroke', 'outer_glow'],
81
- description: 'Which layer style to apply.',
92
+ enum: ['drop_shadow', 'stroke', 'outer_glow', 'inner_shadow', 'inner_glow', 'color_overlay'],
93
+ description: 'Which layer style to apply. drop_shadow/inner_shadow share angle/distance/spread/size; outer_glow/inner_glow share glow_size/glow_spread; color_overlay uses only color + opacity. inner_shadow = recessed shadow inside the layer edges (Multiply); inner_glow = glow inset from the edges (Screen); color_overlay = flat solid-color fill (Normal).',
82
94
  },
83
95
  color: {
84
96
  type: 'object',
@@ -99,24 +111,24 @@ const addLayerStyleSchema = {
99
111
  },
100
112
  angle: {
101
113
  type: 'number',
102
- description: 'drop_shadow only: light angle in degrees. 90 = top-down (standard product shadow).',
114
+ description: 'drop_shadow/inner_shadow: light angle in degrees. 90 = top-down (standard product shadow).',
103
115
  default: 90,
104
116
  },
105
117
  distance: {
106
118
  type: 'number',
107
- description: 'drop_shadow only: shadow offset in pixels.',
119
+ description: 'drop_shadow/inner_shadow: shadow offset in pixels.',
108
120
  default: 8,
109
121
  },
110
122
  spread: {
111
123
  type: 'number',
112
124
  minimum: 0,
113
125
  maximum: 100,
114
- description: 'drop_shadow only: shadow spread (0-100%).',
126
+ description: 'drop_shadow/inner_shadow: shadow choke/spread (0-100%).',
115
127
  default: 0,
116
128
  },
117
129
  size: {
118
130
  type: 'number',
119
- description: 'drop_shadow only: shadow blur size in pixels.',
131
+ description: 'drop_shadow/inner_shadow: shadow blur size in pixels.',
120
132
  default: 12,
121
133
  },
122
134
  stroke_size: {
@@ -132,14 +144,14 @@ const addLayerStyleSchema = {
132
144
  },
133
145
  glow_size: {
134
146
  type: 'number',
135
- description: 'outer_glow only: glow blur size in pixels.',
147
+ description: 'outer_glow/inner_glow: glow blur size in pixels.',
136
148
  default: 12,
137
149
  },
138
150
  glow_spread: {
139
151
  type: 'number',
140
152
  minimum: 0,
141
153
  maximum: 100,
142
- description: 'outer_glow only: glow spread (0-100%).',
154
+ description: 'outer_glow/inner_glow: glow choke/spread (0-100%).',
143
155
  default: 0,
144
156
  },
145
157
  },
@@ -182,9 +194,9 @@ export function createLayerPropertiesTools(connection, snippetClient) {
182
194
  return [
183
195
  {
184
196
  tool: {
185
- name: 'photoshop_convert_to_smart_object',
186
- description: 'Convert the active layer to a Smart Object. This wraps the layer content so every subsequent filter becomes a Smart Filter — fully editable and non-destructive. PREREQUISITE for Camera Raw Filter, non-destructive frequency separation, and any workflow where filter parameters need to stay adjustable after the fact. Works on pixel, text, shape, and adjustment layers. Auto-promotes the background layer if needed. Wrapping an already-Smart-Object layer creates a nested Smart Object (valid in Photoshop — the inner SO is preserved as-is). Use photoshop_rasterize_layer to go the other direction.',
187
- inputSchema: emptySchema,
197
+ name: 'ps_convert_to_smart_object',
198
+ description: 'Convert the active layer to a Smart Object, or (mode=new_via_copy) copy an existing Smart Object to an independent new one. `convert` (default) wraps the layer content so every subsequent filter becomes a Smart Filter — fully editable and non-destructive. PREREQUISITE for Camera Raw Filter, non-destructive frequency separation, and any workflow where filter parameters need to stay adjustable after the fact. Works on pixel, text, shape, and adjustment layers. Auto-promotes the background layer if needed. Wrapping an already-Smart-Object layer creates a nested Smart Object (valid in Photoshop — the inner SO is preserved as-is). `new_via_copy` requires the active layer to already be a Smart Object and yields a copy with its OWN source (unlinked). Use ps_rasterize_layer to go the other direction.',
199
+ inputSchema: convertToSmartObjectSchema,
188
200
  outputSchema: {
189
201
  type: 'object',
190
202
  properties: {
@@ -193,6 +205,7 @@ export function createLayerPropertiesTools(connection, snippetClient) {
193
205
  original_kind: { type: 'string' },
194
206
  was_already_smart_object: { type: 'boolean' },
195
207
  background_promoted: { type: 'boolean' },
208
+ source_unlinked: { type: 'boolean' },
196
209
  context: { type: 'object' },
197
210
  },
198
211
  },
@@ -201,11 +214,11 @@ export function createLayerPropertiesTools(connection, snippetClient) {
201
214
  idempotentHint: false,
202
215
  },
203
216
  },
204
- handler: async () => convertToSmartObject(connection, snippetClient),
217
+ handler: async (args) => convertToSmartObject(connection, snippetClient, args),
205
218
  },
206
219
  {
207
220
  tool: {
208
- name: 'photoshop_rasterize_layer',
221
+ name: 'ps_rasterize_layer',
209
222
  description: 'Rasterize the active layer (convert text or smart object to plain pixels). Destructive — the text/smart-object source is lost. No-op if already a normal raster layer. Returns context so the caller sees the layer kind transition.',
210
223
  inputSchema: emptySchema,
211
224
  outputSchema: {
@@ -228,7 +241,7 @@ export function createLayerPropertiesTools(connection, snippetClient) {
228
241
  },
229
242
  {
230
243
  tool: {
231
- name: 'photoshop_set_layer',
244
+ name: 'ps_set_layer',
232
245
  description: 'Set a property of the active layer — chosen via `property`. Idempotent absolute setters; do not change which layer is active. `opacity`: pass opacity (whole layer incl. styles) and/or fill_percent (pixels only — Photoshop\'s separate "Fill" slider, for Hard Mix / stroke-only effects). `blend_mode`: how the layer composites below (NORMAL to reset). `visibility`: show/hide via `visible`. `locked`: allLocked via `locked`. `name`: rename via `name`. Returns context so the LLM keeps state awareness.',
233
246
  inputSchema: SET_LAYER_INPUT_SCHEMA,
234
247
  outputSchema: {
@@ -257,7 +270,7 @@ export function createLayerPropertiesTools(connection, snippetClient) {
257
270
  },
258
271
  {
259
272
  tool: {
260
- name: 'photoshop_duplicate_layer',
273
+ name: 'ps_duplicate_layer',
261
274
  description: 'Duplicate the active layer. The new layer becomes active. Returns context so the caller sees the new active layer. Not idempotent (each call creates another copy).',
262
275
  inputSchema: duplicateLayerSchema,
263
276
  outputSchema: {
@@ -277,7 +290,7 @@ export function createLayerPropertiesTools(connection, snippetClient) {
277
290
  },
278
291
  {
279
292
  tool: {
280
- name: 'photoshop_layer_via_copy',
293
+ name: 'ps_copy_to_new_layer',
281
294
  description: 'Copy the current selection into a NEW layer above the active one — Photoshop\'s "Layer via Copy" (Ctrl+J). The source layer is left untouched and the new copied layer becomes active. With an active selection only the selected pixels are lifted; with no selection it copies the whole active layer (a plain duplicate). Reach for this to isolate a region for independent transforms or filters (stretch, light rays, a local grade) without altering the source. Returns context so the caller sees the new active layer.',
282
295
  inputSchema: emptySchema,
283
296
  outputSchema: {
@@ -300,7 +313,7 @@ export function createLayerPropertiesTools(connection, snippetClient) {
300
313
  },
301
314
  {
302
315
  tool: {
303
- name: 'photoshop_merge',
316
+ name: 'ps_merge',
304
317
  description: 'Merge layers — choose with `mode`. `visible`: collapse all visible layers into one (DESTRUCTIVE; identities lost). `stamp`: merge visible into a NEW layer on top, leaving originals intact (Ctrl+Alt+Shift+E — the non-destructive "final tweak" composite for output sharpening/grain/contrast; prefer this when building on top rather than collapsing). `flatten`: flatten ALL layers into the background (DESTRUCTIVE; transparency filled with bg color — use sparingly, usually prefer `visible` or exporting a flattened copy). Returns context.',
305
318
  inputSchema: MERGE_INPUT_SCHEMA,
306
319
  outputSchema: {
@@ -325,7 +338,7 @@ export function createLayerPropertiesTools(connection, snippetClient) {
325
338
  },
326
339
  {
327
340
  tool: {
328
- name: 'photoshop_bake_layer',
341
+ name: 'ps_bake_layer',
329
342
  description: 'Flatten the active layer\'s CURRENT APPEARANCE — the layer plus any adjustment layers clipped to it plus its layer styles — into a NEW pixel layer named "<name> (baked)". Non-destructive: the originals are left intact. This is how you get real baked pixels out of a non-destructive stack: clip a Hue/Saturation (saturation -100) or an Invert adjustment to a layer, then bake to get a desaturated / inverted PIXEL layer for techniques that need actual pixels (line art, blurred-layer blends). Also works on a lone layer with nothing clipped to it — it rasterizes that layer\'s own appearance (layer styles, smart-object or text content) into a flat pixel copy. The active layer must be top-level (not inside a group).',
330
343
  inputSchema: emptySchema,
331
344
  outputSchema: {
@@ -347,8 +360,8 @@ export function createLayerPropertiesTools(connection, snippetClient) {
347
360
  },
348
361
  {
349
362
  tool: {
350
- name: 'photoshop_add_layer_style',
351
- description: 'Apply a layer style (drop shadow, stroke, or outer glow) to the active layer. Non-destructive and editable. For PHOTO workflows: `stroke` = print-style border around the image (apply to the flattened photo on a transparent canvas, or to a duplicated background); `outer_glow` = atmospheric lift on highlights (halo around the sun, glow around a window or lamp); `drop_shadow` = depth on text overlays and layered composites. Drop shadow uses Multiply blend; outer glow uses Screen; stroke uses Normal. Cannot be applied to a background layer (duplicate or convert it first). Returns context — applying a style changes what exists on the layer (the Lefx descriptor).',
363
+ name: 'ps_add_layer_style',
364
+ description: 'Apply a layer style (drop shadow, stroke, outer glow, inner shadow, inner glow, or color overlay) to the active layer. Non-destructive and editable. For PHOTO workflows: `stroke` = print-style border around the image (apply to the flattened photo on a transparent canvas, or to a duplicated background); `outer_glow` = atmospheric lift on highlights (halo around the sun, glow around a window or lamp); `drop_shadow` = depth on text overlays and layered composites. Drop shadow uses Multiply blend; outer glow uses Screen; stroke uses Normal. Cannot be applied to a background layer (duplicate or convert it first). Returns context — applying a style changes what exists on the layer (the Lefx descriptor).',
352
365
  inputSchema: addLayerStyleSchema,
353
366
  outputSchema: {
354
367
  type: 'object',
@@ -414,15 +427,21 @@ async function mergeLayers(connection, snippetClient, rawArgs) {
414
427
  };
415
428
  }
416
429
  }
417
- async function convertToSmartObject(connection, snippetClient) {
430
+ async function convertToSmartObject(connection, snippetClient, rawArgs) {
418
431
  try {
419
- const script = await snippetClient.build('convertToSmartObject');
432
+ const args = validateArgs(convertToSmartObjectSchema, rawArgs);
433
+ const mode = args.mode ?? 'convert';
434
+ const snippet = mode === 'new_via_copy' ? 'newSmartObjectViaCopy' : 'convertToSmartObject';
435
+ const script = await snippetClient.build(snippet);
420
436
  const result = await runScript(connection, script);
437
+ const verb = mode === 'new_via_copy'
438
+ ? 'New independent Smart Object created via copy'
439
+ : 'Layer converted to Smart Object';
421
440
  return {
422
441
  content: [
423
442
  {
424
443
  type: 'text',
425
- text: `Layer converted to Smart Object.\nResult: ${JSON.stringify(result)}`,
444
+ text: `${verb}.\nResult: ${JSON.stringify(result)}`,
426
445
  },
427
446
  ],
428
447
  structuredContent: result,
@@ -103,8 +103,8 @@ export function createLayerTools(connection, snippetClient) {
103
103
  return [
104
104
  {
105
105
  tool: {
106
- name: 'photoshop_create_layer',
107
- description: 'Create a new empty raster layer above the currently active layer. Non-destructive. Use photoshop_create_text_layer for text, photoshop_add_adjustment_layer for adjustments.',
106
+ name: 'ps_create_layer',
107
+ description: 'Create a new empty raster layer above the currently active layer. Non-destructive. Use ps_create_text_layer for text, ps_add_adjustment_layer for adjustments.',
108
108
  inputSchema: createLayerSchema,
109
109
  outputSchema: {
110
110
  type: 'object',
@@ -123,8 +123,8 @@ export function createLayerTools(connection, snippetClient) {
123
123
  },
124
124
  {
125
125
  tool: {
126
- name: 'photoshop_delete_layer',
127
- description: 'DESTRUCTIVE: Delete a layer. With no arg, deletes the currently active layer (backward-compatible). With `name`, recurses into groups and deletes the first layer matching that name — useful for cleanup workflows where the dead layer is not currently active. Recoverable only via Edit > Undo. To delete a group and all its contents, use photoshop_delete_group.',
126
+ name: 'ps_delete_layer',
127
+ description: 'DESTRUCTIVE: Delete a layer. With no arg, deletes the currently active layer (backward-compatible). With `name`, recurses into groups and deletes the first layer matching that name — useful for cleanup workflows where the dead layer is not currently active. Recoverable only via Edit > Undo. To delete a group and all its contents, use ps_delete_group.',
128
128
  inputSchema: deleteLayerSchema,
129
129
  outputSchema: {
130
130
  type: 'object',
@@ -144,7 +144,7 @@ export function createLayerTools(connection, snippetClient) {
144
144
  },
145
145
  {
146
146
  tool: {
147
- name: 'photoshop_create_text_layer',
147
+ name: 'ps_create_text_layer',
148
148
  description: 'Create a new text layer with the given content, position, and font size. The text is editable (vector). Use photoshop_set_text_font / photoshop_set_text_color afterwards to style.',
149
149
  inputSchema: createTextLayerSchema,
150
150
  outputSchema: {
@@ -167,7 +167,7 @@ export function createLayerTools(connection, snippetClient) {
167
167
  },
168
168
  {
169
169
  tool: {
170
- name: 'photoshop_fill_layer',
170
+ name: 'ps_fill_layer',
171
171
  description: 'Fill the active layer (or the current selection within it) with a solid RGB color. Idempotent for the same color. Throws on fully-locked or text layers — rasterize text first if needed.',
172
172
  inputSchema: fillLayerSchema,
173
173
  outputSchema: {
@@ -187,8 +187,8 @@ export function createLayerTools(connection, snippetClient) {
187
187
  },
188
188
  {
189
189
  tool: {
190
- name: 'photoshop_add_fill_layer',
191
- description: 'Add a non-destructive SOLID COLOR fill layer (an editable, re-colorable content layer — distinct from photoshop_fill_layer, which bakes color into the active pixel layer). The new fill layer becomes active. Use for color washes, base backgrounds, and clipped color overlays. (Gradient and pattern fill types are planned.)',
190
+ name: 'ps_add_fill_layer',
191
+ description: 'Add a non-destructive SOLID COLOR fill layer (an editable, re-colorable content layer — distinct from ps_fill_layer, which bakes color into the active pixel layer). The new fill layer becomes active. Use for color washes, base backgrounds, and clipped color overlays. (Gradient and pattern fill types are planned.)',
192
192
  inputSchema: addFillLayerSchema,
193
193
  outputSchema: {
194
194
  type: 'object',
@@ -209,8 +209,8 @@ export function createLayerTools(connection, snippetClient) {
209
209
  },
210
210
  {
211
211
  tool: {
212
- name: 'photoshop_select_layer',
213
- description: 'Make a layer active by name. Recurses into groups; if a name appears more than once, picks the first match in stack order (top-to-bottom). Throws if no layer by that name exists. Foundational for multi-layer workflows — use after photoshop_duplicate_layer or photoshop_add_adjustment_layer when you need to focus a specific layer before applying further operations.',
212
+ name: 'ps_select_layer',
213
+ description: 'Make a layer active by name. Recurses into groups; if a name appears more than once, picks the first match in stack order (top-to-bottom). Throws if no layer by that name exists. Foundational for multi-layer workflows — use after ps_duplicate_layer or ps_add_adjustment_layer when you need to focus a specific layer before applying further operations.',
214
214
  inputSchema: selectLayerSchema,
215
215
  outputSchema: {
216
216
  type: 'object',