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
@@ -4,7 +4,6 @@ import { jsLit, jsNum } from '../utils/jsx.js';
4
4
  import { runScript } from '../utils/run-script.js';
5
5
  import { TempDir, userOwnedTempRoot } from '../utils/temp.js';
6
6
  import { validateArgs } from '../utils/validate.js';
7
- import { overlayCoordinateGrid } from '../preview/coordinate-grid.js';
8
7
  const COLOR_NAME_HEX = {
9
8
  red: '#FF0000',
10
9
  blue: '#0066FF',
@@ -129,47 +128,6 @@ const previewSchema = {
129
128
  description: "Optional list of visual annotations to draw onto the preview before rendering — rectangles (by explicit bounds OR by layer name), guides (horizontal / vertical reference lines), point markers, and the current document selection. Use this to verify SPATIAL work (placement, alignment, scaling) — pass both your TARGET rect and the ACTUAL placement (e.g. the layer's bounds) in different colors, then visually compare in the returned image. Skips the spatial-self-evaluation trap where the LLM tries to estimate alignment from a plain preview.",
130
129
  items: annotationSchema,
131
130
  },
132
- coordinate_grid: {
133
- type: 'object',
134
- description: 'Overlay a LABELED coordinate grid on the preview (drawn server-side) so you can read exact DOCUMENT-pixel coordinates straight off the lines instead of estimating. Reach for this BEFORE any tool that needs precise coordinates (rectangle/polygon selections, point-driven filters, future warp meshes). Each gridline is labeled with its document-pixel value (top-left origin, matching Photoshop coordinates). Either pass `step_px` (regular grid every N document px), OR `columns`/`rows` (even divisions), OR neither (auto ~10 divisions on the long edge). Composes with `annotations` (those draw first, in Photoshop; the grid is layered on top in Node).',
135
- properties: {
136
- step_px: {
137
- type: 'number',
138
- description: 'Regular grid: line spacing in DOCUMENT pixels. Wins over columns/rows.',
139
- minimum: 1,
140
- },
141
- columns: {
142
- type: 'integer',
143
- description: 'Even-division grid: number of columns (draws columns−1 interior vertical lines). E.g. 3 = rule-of-thirds verticals.',
144
- minimum: 2,
145
- },
146
- rows: {
147
- type: 'integer',
148
- description: 'Even-division grid: number of rows (draws rows−1 interior horizontal lines).',
149
- minimum: 2,
150
- },
151
- label_every: {
152
- type: 'integer',
153
- description: 'Label every k-th line to reduce clutter on dense grids. Default 1 (every line).',
154
- minimum: 1,
155
- default: 1,
156
- },
157
- normalized: {
158
- type: 'boolean',
159
- description: 'Also print 0.0–1.0 normalized fraction labels along the bottom/right edges — for descriptors that take normalized coordinates (e.g. some filter centers). Default false.',
160
- default: false,
161
- },
162
- color: {
163
- type: 'string',
164
- description: 'Hex #RRGGBB tint for the line/label bright component. Default white (with a black shadow/outline so it reads on any background).',
165
- },
166
- cell_labels: {
167
- type: 'boolean',
168
- description: 'Cell-label mode: divide the document into columns×rows cells (default 8×10) and label each A1/B2/… at its corner. Pick the CELL a feature is in (a reliable discrete choice) instead of reading a continuous coordinate — then zoom into that cell to refine. The legend gives the cell→pixel-region mapping. Default false.',
169
- default: false,
170
- },
171
- },
172
- },
173
131
  },
174
132
  };
175
133
  const boundsDiffSchema = {
@@ -239,8 +197,8 @@ export function createPreviewTools(connection, snippetClient) {
239
197
  return [
240
198
  {
241
199
  tool: {
242
- name: 'photoshop_get_preview',
243
- description: 'Render the active Photoshop document as a flattened, downscaled image and return it inline so the calling AI can see the current visual state. Use this to verify edits visually, judge whether adjustments went too far or not far enough, and decide what to do next. Read-only with respect to the working document (renders a duplicate that is closed immediately). The optional `annotations` array draws overlays in document-space coordinates: `rectangle` / `guide` / `point` / `selection` for spatial verification (Bundle Q, 2026-06-02), and `grid` / `composition` for coordinate readout and compositional evaluation (added 2026-06-09 — grid styles `every` / `thirds` / `quarters` / `phi`, composition styles `diagonals` / `triangles` / `fibonacci_grid` / `golden_spiral`). Use spatial annotations to verify ("did the placed image land inside the frame interior?") — pass both target and actual bounds in different colors. Use grid annotations when you need to translate a visual judgment ("subject is in the lower-left third") into pixel coordinates for a downstream tool call. Use composition annotations to evaluate balance, leading lines, focal-point placement against canonical photographic frameworks. The optional `coordinate_grid` overlays a server-side LABELED grid — each line tagged with its exact document-pixel value — so you can read coordinates straight off the image before any coordinate-dense tool call (selections, point-driven filters) instead of estimating. Without annotations or a coordinate grid, behaves identically to a plain preview.',
200
+ name: 'ps_get_preview',
201
+ description: 'Render the active Photoshop document as a flattened, downscaled image and return it inline so the calling AI can see the current visual state. Use this to verify edits visually, judge whether adjustments went too far or not far enough, and decide what to do next. Read-only with respect to the working document (renders a duplicate that is closed immediately). The optional `annotations` array draws overlays in document-space coordinates: `rectangle` / `guide` / `point` / `selection` for spatial verification (Bundle Q, 2026-06-02), and `grid` / `composition` for coordinate readout and compositional evaluation (added 2026-06-09 — grid styles `every` / `thirds` / `quarters` / `phi`, composition styles `diagonals` / `triangles` / `fibonacci_grid` / `golden_spiral`). Use spatial annotations to verify ("did the placed image land inside the frame interior?") — pass both target and actual bounds in different colors. Use grid / composition annotations to EVALUATE composition (balance, thirds, leading lines, focal-point placement) — not to read a coordinate for a tool call. To place or select precisely, do NOT read a pixel off a grid (unreliable) — NAME the intent to the on-device perception tools instead: `ps_detect` (COCO objects → real document-pixel boxes), `ps_select_by_reference` (a named target → a real selection, not a guessed rectangle), plus any spatial-grounding locator / selection tools present in `tools/list`. Without annotations, behaves identically to a plain preview.',
244
202
  inputSchema: previewSchema,
245
203
  outputSchema: {
246
204
  type: 'object',
@@ -250,7 +208,6 @@ export function createPreviewTools(connection, snippetClient) {
250
208
  dimensions: { type: 'string' },
251
209
  bytes: { type: 'number' },
252
210
  annotation_count: { type: 'number' },
253
- coordinate_grid_applied: { type: 'boolean' },
254
211
  context: { type: 'object' },
255
212
  },
256
213
  },
@@ -264,7 +221,7 @@ export function createPreviewTools(connection, snippetClient) {
264
221
  },
265
222
  {
266
223
  tool: {
267
- name: 'photoshop_get_layer_bounds_diff',
224
+ name: 'ps_get_layer_bounds_diff',
268
225
  description: 'Numeric verification: compute the per-edge pixel deltas between a layer\'s actual bounds and a target rectangle. Returns left/top/right/bottom deltas (actual − target), scale ratio (actual_size / target_size), centroid offset, and a one-word verdict ("aligned", "shifted right", "layer too small", etc.). Use this AFTER place_image / scale_layer / move_layer to confirm the operation produced the intended result — far more reliable than eyeballing a preview for fine alignment. Read-only. Layers nested in groups are found by name recursively. Bundle Q (2026-06-02).',
269
226
  inputSchema: boundsDiffSchema,
270
227
  outputSchema: {
@@ -292,7 +249,7 @@ export function createPreviewTools(connection, snippetClient) {
292
249
  },
293
250
  {
294
251
  tool: {
295
- name: 'photoshop_get_histogram',
252
+ name: 'ps_get_histogram',
296
253
  description: "Whole-image (or per-channel) histogram — the quantitative answer to questions get_preview can't reliably answer by eye. Returns 256 bin counts plus mean / stdev / median. Cheap (~50ms), read-only. **Reach for this when**: (a) clipping detection — bins 0 and 255 carry the count of crushed shadows / blown highlights; you can't see clipping reliably in a downsampled JPEG preview; (b) exposure verification after a levels/curves/exposure adjustment — confirm the histogram actually shifted; (c) \"is this image neutral-gray?\" — compare R / G / B channel means; (d) confirming a destructive op did anything (two identical histograms = no-op regardless of the tool's success message). Composite reads the visible flattened image (any color mode). Per-channel reads require the document have that channel.",
297
254
  inputSchema: histogramSchema,
298
255
  outputSchema: {
@@ -323,7 +280,7 @@ export function createPreviewTools(connection, snippetClient) {
323
280
  },
324
281
  {
325
282
  tool: {
326
- name: 'photoshop_compare_regions',
283
+ name: 'ps_compare_regions',
327
284
  description: 'Quantitative region comparison — the NUMERIC verification answer when "does it look right?" via get_preview is too subjective. Captures the histogram of two rectangular regions on the same document and reports per-channel mean / stdev / median for each region plus the deltas. **Reach for this when**: (a) "did the placed image match the room\'s lighting?" — sample a region of the placed image vs a region of the surrounding wall; (b) "did this adjustment do anything visible in the shadows?" — sample the same dark region before and after; (c) "is region A noticeably warmer/cooler than region B?" — read the channel means directly. Single-pixel sampling via a 1×1 rect is supported. Read-only. Bundle Q (2026-06-02).',
328
285
  inputSchema: compareRegionsSchema,
329
286
  outputSchema: {
@@ -795,7 +752,6 @@ async function getPreview(connection, rawArgs) {
795
752
  const maxDimension = args.max_dimension;
796
753
  const quality = args.quality;
797
754
  const annotations = args.annotations ?? [];
798
- const coordinateGrid = args.coordinate_grid;
799
755
  const annotationsScript = buildAnnotationsScript(annotations);
800
756
  const dir = process.platform === 'darwin'
801
757
  ? await TempDir.createWithRoot(userOwnedTempRoot(), 'editmamei-preview-')
@@ -882,37 +838,7 @@ async function getPreview(connection, rawArgs) {
882
838
  `;
883
839
  const execTimeoutMs = annotations.length > 0 ? 90000 : undefined;
884
840
  const result = (await runScript(connection, script, execTimeoutMs));
885
- let bytes = await readFile(tempPath);
886
- let gridNote = '';
887
- let gridApplied = false;
888
- if (coordinateGrid) {
889
- const doc = result?.context
890
- ?.document;
891
- if (typeof doc?.width === 'number' && typeof doc?.height === 'number') {
892
- try {
893
- const grid = overlayCoordinateGrid(bytes, {
894
- docWidth: doc.width,
895
- docHeight: doc.height,
896
- stepPx: coordinateGrid.step_px,
897
- columns: coordinateGrid.columns,
898
- rows: coordinateGrid.rows,
899
- labelEvery: coordinateGrid.label_every,
900
- normalized: coordinateGrid.normalized,
901
- color: coordinateGrid.color,
902
- cellLabels: coordinateGrid.cell_labels,
903
- });
904
- bytes = grid.jpeg;
905
- gridNote = ` ${grid.legend}`;
906
- gridApplied = true;
907
- }
908
- catch (gridErr) {
909
- gridNote = ` (coordinate grid skipped: ${gridErr instanceof Error ? gridErr.message : String(gridErr)})`;
910
- }
911
- }
912
- else {
913
- gridNote = ' (coordinate grid skipped: document dimensions unavailable)';
914
- }
915
- }
841
+ const bytes = await readFile(tempPath);
916
842
  const base64 = bytes.toString('base64');
917
843
  const dims = result?.width_px && result?.height_px
918
844
  ? `${result.width_px}x${result.height_px}`
@@ -924,7 +850,7 @@ async function getPreview(connection, rawArgs) {
924
850
  { type: 'image', data: base64, mimeType: 'image/jpeg' },
925
851
  {
926
852
  type: 'text',
927
- text: `Preview rendered (JPEG, ${dims}, ${formatBytes(bytes.length)}${annNote}). This is the current flattened state of the active document.${gridNote}`,
853
+ text: `Preview rendered (JPEG, ${dims}, ${formatBytes(bytes.length)}${annNote}). This is the current flattened state of the active document.`,
928
854
  },
929
855
  ],
930
856
  structuredContent: {
@@ -933,7 +859,6 @@ async function getPreview(connection, rawArgs) {
933
859
  dimensions: dims,
934
860
  bytes: bytes.length,
935
861
  annotation_count: annotationCount,
936
- coordinate_grid_applied: gridApplied,
937
862
  context: result?.context,
938
863
  },
939
864
  };
@@ -1,5 +1,23 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
+ import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
4
+ import { OnnxDetectionClient } from '../detection/detection-client.js';
5
+ async function groundedOffset(connection, snippetClient, detClient, placement, label) {
6
+ const rp = await resolveExpectedPlacement(connection, detClient, placement, 'point', label);
7
+ const sel = (await runScript(connection, await snippetClient.build('getSelectionState')));
8
+ if (!sel.has_selection || !sel.bounds)
9
+ throw new Error(`${label} needs an active selection to move relative to — none found. Make the selection first.`);
10
+ const center = {
11
+ x: (sel.bounds.left + sel.bounds.right) / 2,
12
+ y: (sel.bounds.top + sel.bounds.bottom) / 2,
13
+ };
14
+ return {
15
+ offsetX: Math.round(rp.point.x - center.x),
16
+ offsetY: Math.round(rp.point.y - center.y),
17
+ point: rp.point,
18
+ center: { x: Math.round(center.x), y: Math.round(center.y) },
19
+ };
20
+ }
3
21
  const APPLY_TO_ACTIVE_LAYER_PROP = {
4
22
  type: 'boolean',
5
23
  description: 'If false (default), the retouch op is applied to a duplicate of the active layer named "<OpName> (<Original Name>)" — the original is preserved and the LLM can undo by deleting the copy. If true, the op bakes directly into the active layer.',
@@ -62,13 +80,17 @@ const contentAwareFillSchema = {
62
80
  const patchSchema = {
63
81
  type: 'object',
64
82
  properties: {
83
+ source_placement: {
84
+ ...PLACEMENT_SCHEMA,
85
+ description: 'Grounded alternative to offset_x/offset_y: NAME the patch SOURCE (resolves to a POINT via the grounding resolver + objective gate — e.g. an extremum for the cleanest nearby skin, a grid intersection). The offset from the selection center to that point is computed for you. Wins over offset_x/offset_y. Provide THIS or offset_x + offset_y.',
86
+ },
65
87
  offset_x: {
66
88
  type: 'number',
67
- description: 'Horizontal pixel offset from the selection to the patch source. Positive = source is to the right. Required.',
89
+ description: 'Horizontal pixel offset from the selection to the patch source. Positive = source is to the right. Required unless source_placement is given.',
68
90
  },
69
91
  offset_y: {
70
92
  type: 'number',
71
- description: 'Vertical pixel offset from the selection to the patch source. Positive = source is below. Required.',
93
+ description: 'Vertical pixel offset from the selection to the patch source. Positive = source is below. Required unless source_placement is given.',
72
94
  },
73
95
  patch_structure: {
74
96
  type: 'integer',
@@ -108,18 +130,21 @@ const patchSchema = {
108
130
  },
109
131
  apply_to_active_layer: APPLY_TO_ACTIVE_LAYER_PROP,
110
132
  },
111
- required: ['offset_x', 'offset_y'],
112
133
  };
113
134
  const contentAwareMoveSchema = {
114
135
  type: 'object',
115
136
  properties: {
137
+ destination_placement: {
138
+ ...PLACEMENT_SCHEMA,
139
+ description: 'Grounded alternative to offset_x/offset_y: NAME where to move the selection (resolves to a POINT via the grounding resolver + objective gate — a grid intersection, an object centroid, an extremum). The selection center is moved ONTO that point; the offset is computed for you. Wins over offset_x/offset_y. Provide THIS or offset_x + offset_y.',
140
+ },
116
141
  offset_x: {
117
142
  type: 'number',
118
- description: 'Horizontal pixel delta to move the selected content. Positive = right.',
143
+ description: 'Horizontal pixel delta to move the selected content. Positive = right. Required unless destination_placement is given.',
119
144
  },
120
145
  offset_y: {
121
146
  type: 'number',
122
- description: 'Vertical pixel delta to move the selected content. Positive = down.',
147
+ description: 'Vertical pixel delta to move the selected content. Positive = down. Required unless destination_placement is given.',
123
148
  },
124
149
  patch_structure: {
125
150
  type: 'integer',
@@ -159,7 +184,6 @@ const contentAwareMoveSchema = {
159
184
  },
160
185
  apply_to_active_layer: APPLY_TO_ACTIVE_LAYER_PROP,
161
186
  },
162
- required: ['offset_x', 'offset_y'],
163
187
  };
164
188
  const RETOUCH_OUTPUT_SCHEMA = {
165
189
  type: 'object',
@@ -217,11 +241,21 @@ async function applyContentAwareFill(connection, snippetClient, rawArgs) {
217
241
  };
218
242
  }
219
243
  }
220
- async function applyPatch(connection, snippetClient, rawArgs) {
244
+ async function applyPatch(connection, snippetClient, detClient, rawArgs) {
221
245
  try {
222
246
  const args = validateArgs(patchSchema, rawArgs);
223
- const offsetX = args.offset_x;
224
- const offsetY = args.offset_y;
247
+ let offsetX = args.offset_x;
248
+ let offsetY = args.offset_y;
249
+ let grounded;
250
+ if (args.source_placement) {
251
+ const g = await groundedOffset(connection, snippetClient, detClient, args.source_placement, 'patch source');
252
+ offsetX = g.offsetX;
253
+ offsetY = g.offsetY;
254
+ grounded = { point: g.point, center: g.center };
255
+ }
256
+ else if (offsetX === undefined || offsetY === undefined) {
257
+ throw new Error('provide offset_x + offset_y, or source_placement to NAME the patch source region.');
258
+ }
225
259
  const patchStructure = args.patch_structure ?? 5;
226
260
  const patchColor = args.patch_color ?? 5;
227
261
  const healSmoothFactor = args.heal_smooth_factor ?? 5;
@@ -245,14 +279,26 @@ async function applyPatch(connection, snippetClient, rawArgs) {
245
279
  const target = r.target_was_copy
246
280
  ? `new copy "${r.target_layer_name ?? '?'}"`
247
281
  : 'active layer (in place)';
282
+ const groundedNote = grounded
283
+ ? ` — source at (${Math.round(grounded.point.x)},${Math.round(grounded.point.y)}) via placement (gate PASS)`
284
+ : '';
285
+ const structured = result;
286
+ if (grounded) {
287
+ structured.source_placement = {
288
+ gate: { pass: true },
289
+ point: { x: Math.round(grounded.point.x), y: Math.round(grounded.point.y) },
290
+ selection_center: grounded.center,
291
+ offset: { x: offsetX, y: offsetY },
292
+ };
293
+ }
248
294
  return {
249
295
  content: [
250
296
  {
251
297
  type: 'text',
252
- text: `Patch applied to ${target} (offset ${offsetX}, ${offsetY}; structure ${patchStructure}, color ${patchColor}).`,
298
+ text: `Patch applied to ${target} (offset ${offsetX}, ${offsetY}; structure ${patchStructure}, color ${patchColor})${groundedNote}.`,
253
299
  },
254
300
  ],
255
- structuredContent: result,
301
+ structuredContent: structured,
256
302
  };
257
303
  }
258
304
  catch (error) {
@@ -267,11 +313,21 @@ async function applyPatch(connection, snippetClient, rawArgs) {
267
313
  };
268
314
  }
269
315
  }
270
- async function applyContentAwareMove(connection, snippetClient, rawArgs) {
316
+ async function applyContentAwareMove(connection, snippetClient, detClient, rawArgs) {
271
317
  try {
272
318
  const args = validateArgs(contentAwareMoveSchema, rawArgs);
273
- const offsetX = args.offset_x;
274
- const offsetY = args.offset_y;
319
+ let offsetX = args.offset_x;
320
+ let offsetY = args.offset_y;
321
+ let grounded;
322
+ if (args.destination_placement) {
323
+ const g = await groundedOffset(connection, snippetClient, detClient, args.destination_placement, 'content-aware move');
324
+ offsetX = g.offsetX;
325
+ offsetY = g.offsetY;
326
+ grounded = { point: g.point, center: g.center };
327
+ }
328
+ else if (offsetX === undefined || offsetY === undefined) {
329
+ throw new Error('provide offset_x + offset_y, or destination_placement to NAME where to move the selection.');
330
+ }
275
331
  const patchStructure = args.patch_structure ?? 4;
276
332
  const patchColor = args.patch_color ?? 5;
277
333
  const healSmoothFactor = args.heal_smooth_factor ?? 5;
@@ -295,14 +351,26 @@ async function applyContentAwareMove(connection, snippetClient, rawArgs) {
295
351
  const target = r.target_was_copy
296
352
  ? `new copy "${r.target_layer_name ?? '?'}"`
297
353
  : 'active layer (in place)';
354
+ const groundedNote = grounded
355
+ ? ` — moved onto (${Math.round(grounded.point.x)},${Math.round(grounded.point.y)}) via placement (gate PASS)`
356
+ : '';
357
+ const structured = result;
358
+ if (grounded) {
359
+ structured.destination_placement = {
360
+ gate: { pass: true },
361
+ point: { x: Math.round(grounded.point.x), y: Math.round(grounded.point.y) },
362
+ selection_center: grounded.center,
363
+ offset: { x: offsetX, y: offsetY },
364
+ };
365
+ }
298
366
  return {
299
367
  content: [
300
368
  {
301
369
  type: 'text',
302
- text: `Content-Aware Move applied to ${target} (delta ${offsetX}, ${offsetY}).`,
370
+ text: `Content-Aware Move applied to ${target} (delta ${offsetX}, ${offsetY})${groundedNote}.`,
303
371
  },
304
372
  ],
305
- structuredContent: result,
373
+ structuredContent: structured,
306
374
  };
307
375
  }
308
376
  catch (error) {
@@ -326,8 +394,8 @@ const RETOUCH_INPUT_SCHEMA = {
326
394
  enum: [...RETOUCH_METHODS],
327
395
  description: 'Retouch technique (all require an active selection). ' +
328
396
  'content_aware_fill: synthesize a fill from surrounding content (color_adaptation, opacity, blend_mode, rotate/scale/mirror). ' +
329
- 'patch: offset_x + offset_y REQUIRED — sample replacement pixels from that offset (patch_structure 1-7, patch_color 0-10, use_source). ' +
330
- 'content_aware_move: offset_x + offset_y REQUIRED — relocate the selection and fill the source (patch_structure, reshuffle).',
397
+ 'patch: sample replacement pixels — offset_x + offset_y, OR source_placement to NAME the clean source region (patch_structure 1-7, patch_color 0-10, use_source). ' +
398
+ 'content_aware_move: relocate the selection and fill the source — offset_x + offset_y, OR destination_placement to NAME where to move it (patch_structure, reshuffle).',
331
399
  },
332
400
  ...contentAwareFillSchema.properties,
333
401
  ...patchSchema.properties,
@@ -335,12 +403,12 @@ const RETOUCH_INPUT_SCHEMA = {
335
403
  },
336
404
  required: ['method'],
337
405
  };
338
- export function createRetouchTools(connection, snippetClient) {
406
+ export function createRetouchTools(connection, snippetClient, detClient = new OnnxDetectionClient()) {
339
407
  return [
340
408
  {
341
409
  tool: {
342
- name: 'photoshop_retouch',
343
- description: 'Selection-driven content-aware retouch — choose the technique with `method`. Requires an active selection (throws if none, so Content-Aware Fill never silently fills the whole layer). Auto-duplicates the active layer by default so the original is preserved (auto-duplicate-first per Bundle O); pass `apply_to_active_layer: true` to bake into the original. `content_aware_fill` synthesizes a fill from surroundings (remove wires/objects/blemishes); `patch` samples replacement pixels from a chosen offset (skin retouch, known-clean source); `content_aware_move` relocates the selection and fills the source (reposition a subject without manual compositing).',
410
+ name: 'ps_retouch',
411
+ description: 'Selection-driven content-aware retouch — choose the technique with `method`. Requires an active selection (throws if none, so Content-Aware Fill never silently fills the whole layer). Auto-duplicates the active layer by default so the original is preserved (auto-duplicate-first per Bundle O); pass `apply_to_active_layer: true` to bake into the original. `content_aware_fill` synthesizes a fill from surroundings (remove wires/objects/blemishes); `patch` samples replacement pixels from a chosen offset — or NAME the source region with `source_placement` (grounded + gated); `content_aware_move` relocates the selection and fills the source — offset, or NAME where with `destination_placement`.',
344
412
  inputSchema: RETOUCH_INPUT_SCHEMA,
345
413
  outputSchema: RETOUCH_OUTPUT_SCHEMA,
346
414
  annotations: {
@@ -349,20 +417,20 @@ export function createRetouchTools(connection, snippetClient) {
349
417
  idempotentHint: false,
350
418
  },
351
419
  },
352
- handler: async (args) => retouch(connection, snippetClient, args),
420
+ handler: async (args) => retouch(connection, snippetClient, detClient, args),
353
421
  },
354
422
  ];
355
423
  }
356
- async function retouch(connection, snippetClient, rawArgs) {
424
+ async function retouch(connection, snippetClient, detClient, rawArgs) {
357
425
  const method = rawArgs.method;
358
426
  const { method: _omit, ...rest } = rawArgs;
359
427
  switch (method) {
360
428
  case 'content_aware_fill':
361
429
  return applyContentAwareFill(connection, snippetClient, rest);
362
430
  case 'patch':
363
- return applyPatch(connection, snippetClient, rest);
431
+ return applyPatch(connection, snippetClient, detClient, rest);
364
432
  case 'content_aware_move':
365
- return applyContentAwareMove(connection, snippetClient, rest);
433
+ return applyContentAwareMove(connection, snippetClient, detClient, rest);
366
434
  default:
367
435
  return {
368
436
  content: [
@@ -9,7 +9,7 @@ function makeProRefine(invokeTool) {
9
9
  try {
10
10
  if (req.kind === 'subject') {
11
11
  const which = typeof req.instance === 'number' ? String(req.instance) : 'largest';
12
- const res = await invokeTool('photoshop_select_subject_instance', {
12
+ const res = await invokeTool('ps_select_subject_instance', {
13
13
  label: req.label,
14
14
  which,
15
15
  save_as: 'selection',
@@ -17,7 +17,7 @@ function makeProRefine(invokeTool) {
17
17
  return res.isError !== true;
18
18
  }
19
19
  const feature = req.kind === 'face' ? 'face' : req.feature;
20
- const res = await invokeTool('photoshop_select_face_feature', {
20
+ const res = await invokeTool('ps_select_face_feature', {
21
21
  feature,
22
22
  which: 'best',
23
23
  save_as: 'selection',
@@ -32,7 +32,7 @@ function makeProRefine(invokeTool) {
32
32
  function makePrecomputeFace(invokeTool) {
33
33
  return async () => {
34
34
  try {
35
- const res = await invokeTool('photoshop_select_face_feature', { precompute_all: true });
35
+ const res = await invokeTool('ps_select_face_feature', { precompute_all: true });
36
36
  if (res.isError)
37
37
  return [];
38
38
  const saved = res.structuredContent?.saved ?? [];
@@ -73,11 +73,11 @@ const sceneSchema = {
73
73
  save_regions: {
74
74
  type: 'boolean',
75
75
  default: true,
76
- description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence). photoshop_select_by_reference then loads the saved channel instantly. Set false for a light read with no channels.',
76
+ description: 'Precompute every confident region (sky/ground/shadows/highlights/skin/subject/face — and, on a Pro host with a face, the face-feature set scene:face_skin/_eyes/_brows/_lips/_teeth/_nose/_under_eye/_cheeks) and SAVE each as a managed `scene:*` alpha channel, returning the `regions` MENU of what is confidently selectable (each with its method + confidence). ps_select_by_reference then loads the saved channel instantly. Set false for a light read with no channels.',
77
77
  },
78
78
  composition_context: {
79
79
  type: 'object',
80
- description: 'Optional: tune the confidence gate for an artistic/non-standard composition (e.g. profile:big_sky) so a legitimately large region is not rejected. Same shape as photoshop_select_by_reference.',
80
+ description: 'Optional: tune the confidence gate for an artistic/non-standard composition (e.g. profile:big_sky) so a legitimately large region is not rejected. Same shape as ps_select_by_reference.',
81
81
  properties: {
82
82
  profile: {
83
83
  type: 'string',
@@ -149,7 +149,7 @@ function summarizeScene(model) {
149
149
  ];
150
150
  if (main && cell)
151
151
  parts.push(`main subject in the ${cell.row}-${cell.col} third`);
152
- return `Scene (${model.doc.width}×${model.doc.height}): ${parts.join('; ')}. Select named regions with photoshop_select_by_reference.`;
152
+ return `Scene (${model.doc.width}×${model.doc.height}): ${parts.join('; ')}. Select named regions with ps_select_by_reference.`;
153
153
  }
154
154
  async function scene(connection, snippet, client, rawArgs, proRefine, precomputeFace) {
155
155
  try {
@@ -284,7 +284,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
284
284
  content: [
285
285
  {
286
286
  type: 'text',
287
- text: `Selected "${target}" from the saved scene:${target} channel (precomputed by photoshop_scene). Verify with photoshop_get_selection_preview.`,
287
+ text: `Selected "${target}" from the saved scene:${target} channel (precomputed by ps_read_scene). Verify with ps_get_selection_preview.`,
288
288
  },
289
289
  ],
290
290
  structuredContent: {
@@ -314,7 +314,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
314
314
  });
315
315
  const conf = res.confidence.toFixed(2);
316
316
  const summary = res.passed
317
- ? `Selected "${target}" via ${res.method} (confidence ${conf}). Verify with photoshop_get_selection_preview.`
317
+ ? `Selected "${target}" via ${res.method} (confidence ${conf}). Verify with ps_get_selection_preview.`
318
318
  : `No confident "${target}" in this image (confidence ${conf}, below the gate) — nothing selected. ${res.reasons.join('; ')}. If this is an artistic/non-standard composition, pass composition_context to relax the prior.`;
319
319
  return {
320
320
  content: [{ type: 'text', text: summary }],
@@ -334,7 +334,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
334
334
  catch (error) {
335
335
  const msg = error instanceof Error ? error.message : String(error);
336
336
  return {
337
- content: [{ type: 'text', text: `Error in photoshop_select_by_reference: ${msg}` }],
337
+ content: [{ type: 'text', text: `Error in ps_select_by_reference: ${msg}` }],
338
338
  isError: true,
339
339
  };
340
340
  }
@@ -345,8 +345,8 @@ export function createSceneTools(connection, snippetClient, client = new OnnxDet
345
345
  return [
346
346
  {
347
347
  tool: {
348
- name: 'photoshop_scene',
349
- description: 'Build a structured SCENE MODEL of the active document using LOCAL on-device vision + classical CV — the "look before you select" read. Returns detected subjects (with the main one flagged) and faces in document pixels, a coarse sky/ground region map, the horizon line (y + placement + confidence), tonal zones (shadow/midtone/highlight bands + coverage), and composition geometry (which thirds cell the subject sits in, balance, headroom), plus an annotated preview. The image never leaves the machine. Read-only: renders a throwaway duplicate. Run this first to SEE the menu of selectable named regions, then select them by name with photoshop_select_by_reference instead of guessing a rectangle. Perception is cached per document state, so repeated reads are cheap.',
348
+ name: 'ps_read_scene',
349
+ description: 'Build a structured SCENE MODEL of the active document using LOCAL on-device vision + classical CV — the "look before you select" read. Returns detected subjects (with the main one flagged) and faces in document pixels, a coarse sky/ground region map, the horizon line (y + placement + confidence), tonal zones (shadow/midtone/highlight bands + coverage), and composition geometry (which thirds cell the subject sits in, balance, headroom), plus an annotated preview. The image never leaves the machine. Read-only: renders a throwaway duplicate. Run this first to SEE the menu of selectable named regions, then select them by name with ps_select_by_reference instead of guessing a rectangle. Perception is cached per document state, so repeated reads are cheap.',
350
350
  inputSchema: sceneSchema,
351
351
  outputSchema: {
352
352
  type: 'object',
@@ -372,8 +372,8 @@ export function createSceneTools(connection, snippetClient, client = new OnnxDet
372
372
  },
373
373
  {
374
374
  tool: {
375
- name: 'photoshop_select_by_reference',
376
- description: 'Select a region by NAME instead of coordinates — the natural-mask alternative to a rectangle — with a CONFIDENCE GATE. target=sky/ground/foliage/subject/face/shadows/highlights/skin/above_horizon resolves through the right Photoshop-native method (threshold for sky, invert-sky−subjects for ground, luminance for shadows/highlights, skin-tone colour ∩ the subject box, the detected face/subject box) and is SCORED before it is offered: a clean region is left selected; an unconfident one is NOT selected and reported as honest absence (the city with no real sky gets no sky). Pro adds precise FACE-FEATURE targets backed by the face mesh — face_skin (the retouch mask: face minus eyes/brows/lips), face_eyes, face_brows, face_lips, face_teeth (mouth opening), face_nose, face_under_eye, face_cheeks — each a real geometry-following selection, loaded instantly from the scene:face_* channel photoshop_scene precomputes. `passed`/`confidence` are returned. The structural floor (coherence, horizon alignment) is never tuned; for an artistic/non-standard shot pass `composition_context` (e.g. profile:big_sky) to relax the compositional priors so a legitimately large sky is not rejected. For target=subject with several present, pass `label` and/or `instance`. Build/inspect with photoshop_scene first; verify with photoshop_get_selection_preview (the red-overlay is the human/agent oversight view). Prefer this over a rectangle for any real-world region.',
375
+ name: 'ps_select_by_reference',
376
+ description: 'Select a region by NAME instead of coordinates — the natural-mask alternative to a rectangle — with a CONFIDENCE GATE. target=sky/ground/foliage/subject/face/shadows/highlights/skin/above_horizon resolves through the right Photoshop-native method (threshold for sky, invert-sky−subjects for ground, luminance for shadows/highlights, skin-tone colour ∩ the subject box, the detected face/subject box) and is SCORED before it is offered: a clean region is left selected; an unconfident one is NOT selected and reported as honest absence (the city with no real sky gets no sky). Pro adds precise FACE-FEATURE targets backed by the face mesh — face_skin (the retouch mask: face minus eyes/brows/lips), face_eyes, face_brows, face_lips, face_teeth (mouth opening), face_nose, face_under_eye, face_cheeks — each a real geometry-following selection, loaded instantly from the scene:face_* channel ps_read_scene precomputes. `passed`/`confidence` are returned. The structural floor (coherence, horizon alignment) is never tuned; for an artistic/non-standard shot pass `composition_context` (e.g. profile:big_sky) to relax the compositional priors so a legitimately large sky is not rejected. For target=subject with several present, pass `label` and/or `instance`. Build/inspect with ps_read_scene first; verify with ps_get_selection_preview (the red-overlay is the human/agent oversight view). Prefer this over a rectangle for any real-world region.',
377
377
  inputSchema: selectByReferenceSchema,
378
378
  outputSchema: {
379
379
  type: 'object',