editmamei 0.20.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 (65) hide show
  1. package/NOTICES.md +17 -1
  2. package/README.md +8 -7
  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/install.js +2 -2
  9. package/dist/core/server.js +7 -7
  10. package/dist/core/settings.js +2 -2
  11. package/dist/core/tool-groups.js +91 -82
  12. package/dist/core/tool-tiers.js +91 -82
  13. package/dist/detection/landmark-detection-client.js +8 -0
  14. package/dist/detection/mesh-eye-correction.js +126 -0
  15. package/dist/detection/sam-segmenter.js +83 -0
  16. package/dist/index.js +2 -0
  17. package/dist/license/entitlement.js +52 -0
  18. package/dist/modules/ce/index.js +4 -4
  19. package/dist/perception/grounding-anchors.js +37 -0
  20. package/dist/perception/grounding-corners.js +93 -0
  21. package/dist/perception/grounding-edge-trace.js +90 -0
  22. package/dist/perception/grounding-extrema.js +41 -0
  23. package/dist/perception/grounding-gate.js +154 -0
  24. package/dist/perception/grounding-geometry.js +132 -0
  25. package/dist/perception/grounding-grid.js +34 -0
  26. package/dist/perception/grounding-landmarks.js +40 -0
  27. package/dist/perception/grounding-locate.js +341 -0
  28. package/dist/perception/grounding-resolver.js +202 -0
  29. package/dist/perception/grounding-review-crop.js +85 -0
  30. package/dist/perception/grounding-warp.js +182 -0
  31. package/dist/perception/sky-mask-transfer.js +50 -18
  32. package/dist/skills/editmamei-skill.zip +0 -0
  33. package/dist/tools/adjustment-tools.js +3 -3
  34. package/dist/tools/brush-tools.js +111 -23
  35. package/dist/tools/channel-compose-tools.js +241 -0
  36. package/dist/tools/detection-tools.js +1 -1
  37. package/dist/tools/diagnostics-tools.js +1 -1
  38. package/dist/tools/document-tools.js +6 -6
  39. package/dist/tools/filter-tools.js +476 -19
  40. package/dist/tools/group-tools.js +10 -10
  41. package/dist/tools/guide-tools.js +4 -4
  42. package/dist/tools/history-tools.js +4 -4
  43. package/dist/tools/image-placement-tools.js +1 -1
  44. package/dist/tools/image-tools.js +62 -19
  45. package/dist/tools/inspect-tools.js +2 -2
  46. package/dist/tools/layer-ordering-tools.js +1 -1
  47. package/dist/tools/layer-properties-tools.js +42 -23
  48. package/dist/tools/layer-tools.js +10 -10
  49. package/dist/tools/layer-transform-tools.js +80 -41
  50. package/dist/tools/overview-tools.js +42 -42
  51. package/dist/tools/path-tools.js +53 -10
  52. package/dist/tools/portrait-tools.js +3 -3
  53. package/dist/tools/preview-tools.js +7 -82
  54. package/dist/tools/retouch-tools.js +93 -25
  55. package/dist/tools/scene-tools.js +13 -13
  56. package/dist/tools/selection-tools.js +319 -40
  57. package/dist/tools/shape-tools.js +216 -0
  58. package/dist/tools/text-tools.js +1 -1
  59. package/dist/tools/transform-canvas-tools.js +2 -2
  60. package/dist/tools/vector-mask-tools.js +23 -8
  61. package/dist/version.js +1 -1
  62. package/package.json +1 -1
  63. package/dist/preview/coordinate-grid.js +0 -250
  64. package/dist/tools/object-tools.js +0 -193
  65. package/dist/tools/text-on-object-tools.js +0 -225
@@ -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 fitLayerToDocumentSchema = {
4
6
  type: 'object',
5
7
  properties: {
@@ -74,6 +76,13 @@ const moveLayerSchema = {
74
76
  type: 'number',
75
77
  description: "CENTER mode: target Y for the layer's bounds CENTER point, in document pixels. Pair with center_on_x.",
76
78
  },
79
+ placement: {
80
+ ...PLACEMENT_SCHEMA,
81
+ description: 'ANCHOR-RELATIONAL move (preferred over guessing a pixel): a POINT relation (centroid/midpoint/offset) → ' +
82
+ 'the layer\'s CENTER is moved to the resolved, gate-verified point (e.g. "center this layer on the detected ' +
83
+ 'subject" / "…in the gap between the two people"). Moves ONLY if the gate PASSES. When set, delta_*/' +
84
+ 'absolute_*/center_on_* are ignored. See ps_resolve_placement for the anchors + relation vocabulary.',
85
+ },
77
86
  },
78
87
  };
79
88
  const rotateLayerSchema = {
@@ -157,12 +166,12 @@ const TRANSFORM_INPUT_SCHEMA = {
157
166
  },
158
167
  required: ['op'],
159
168
  };
160
- export function createLayerTransformTools(connection, snippetClient) {
169
+ export function createLayerTransformTools(connection, snippetClient, client = new OnnxLandmarkDetectionClient()) {
161
170
  return [
162
171
  {
163
172
  tool: {
164
- name: 'photoshop_transform_layer',
165
- description: 'Transform the active layer — choose the operation with `op`. `fit` scales to fit (letterbox) or fill (crop) the canvas and centers (idempotent). `scale` does uniform (scale_percent) or non-uniform stretch/squash (scale_x_percent/scale_y_percent) — multiplicative, not idempotent. `move` translates via exactly one of delta (relative), absolute (top-left target), or center_on (center target — best for "place inside the frame opening"); mixing modes errors. `rotate` rotates by relative degrees around center. `flip` mirrors horizontal/vertical. `skew` slants the layer (skew_h_degrees/skew_v_degrees). `free` is a numeric free-transform (scale + degrees + offset). scale/move/rotate/flip/skew/free auto-promote the background layer (background_promoted=true).',
173
+ name: 'ps_transform_layer',
174
+ description: 'Transform the active layer — choose the operation with `op`. `fit` scales to fit (letterbox) or fill (crop) the canvas and centers (idempotent). `scale` does uniform (scale_percent) or non-uniform stretch/squash (scale_x_percent/scale_y_percent) — multiplicative, not idempotent. `move` translates via exactly one of delta (relative), absolute (top-left target), center_on (center target — best for "place inside the frame opening"), or an anchor-relational `placement` (preferred: a point relation → the layer center moves to the resolved, gate-verified point); mixing the raw modes errors. `rotate` rotates by relative degrees around center. `flip` mirrors horizontal/vertical. `skew` slants the layer (skew_h_degrees/skew_v_degrees). `free` is a numeric free-transform (scale + degrees + offset). scale/move/rotate/flip/skew/free auto-promote the background layer (background_promoted=true).',
166
175
  inputSchema: TRANSFORM_INPUT_SCHEMA,
167
176
  outputSchema: {
168
177
  type: 'object',
@@ -190,6 +199,16 @@ export function createLayerTransformTools(connection, snippetClient) {
190
199
  applied_delta_y: { type: 'number' },
191
200
  new_bounds: { type: 'object' },
192
201
  background_promoted: { type: 'boolean' },
202
+ placement: {
203
+ type: 'object',
204
+ description: 'Present when op=move used anchor-relational placement: the resolved point + gate verdict.',
205
+ properties: {
206
+ target: { type: 'string' },
207
+ gate: { type: 'object' },
208
+ anchors: { type: 'object' },
209
+ geometry: { type: 'object' },
210
+ },
211
+ },
193
212
  context: { type: 'object' },
194
213
  },
195
214
  },
@@ -198,11 +217,11 @@ export function createLayerTransformTools(connection, snippetClient) {
198
217
  idempotentHint: false,
199
218
  },
200
219
  },
201
- handler: async (args) => transformLayer(connection, snippetClient, args),
220
+ handler: async (args) => transformLayer(connection, snippetClient, client, args),
202
221
  },
203
222
  ];
204
223
  }
205
- async function transformLayer(connection, snippetClient, rawArgs) {
224
+ async function transformLayer(connection, snippetClient, client, rawArgs) {
206
225
  const op = rawArgs.op;
207
226
  const { op: _omit, ...rest } = rawArgs;
208
227
  switch (op) {
@@ -211,7 +230,7 @@ async function transformLayer(connection, snippetClient, rawArgs) {
211
230
  case 'scale':
212
231
  return scaleLayer(connection, snippetClient, rest);
213
232
  case 'move':
214
- return moveLayer(connection, snippetClient, rest);
233
+ return moveLayer(connection, snippetClient, client, rest);
215
234
  case 'rotate':
216
235
  return rotateLayer(connection, snippetClient, rest);
217
236
  case 'flip':
@@ -338,39 +357,47 @@ async function flipLayer(connection, snippetClient, rawArgs) {
338
357
  };
339
358
  }
340
359
  }
341
- async function moveLayer(connection, snippetClient, rawArgs) {
360
+ async function moveLayer(connection, snippetClient, client, rawArgs) {
342
361
  try {
343
362
  const args = validateArgs(moveLayerSchema, rawArgs);
344
- const hasDelta = args.delta_x !== undefined || args.delta_y !== undefined;
345
- const hasAbsolute = args.absolute_x !== undefined || args.absolute_y !== undefined;
346
- const hasCenter = args.center_on_x !== undefined || args.center_on_y !== undefined;
347
- const modeCount = (hasDelta ? 1 : 0) + (hasAbsolute ? 1 : 0) + (hasCenter ? 1 : 0);
348
- if (modeCount === 0) {
349
- throw new Error('No positioning mode specified. Pass exactly one pair: delta_x+delta_y (relative), absolute_x+absolute_y (target top-left), or center_on_x+center_on_y (target center).');
350
- }
351
- if (modeCount > 1) {
352
- throw new Error('Mixed positioning modes. Pass exactly ONE pair: delta_x+delta_y OR absolute_x+absolute_y OR center_on_x+center_on_y.');
353
- }
354
- if (hasDelta && (args.delta_x === undefined || args.delta_y === undefined)) {
355
- throw new Error('Delta mode requires both delta_x and delta_y.');
363
+ let mode;
364
+ let deltaX = 0, deltaY = 0, absoluteX = 0, absoluteY = 0, centerOnX = 0, centerOnY = 0;
365
+ let placementInfo;
366
+ if (args.placement) {
367
+ const rp = await resolveExpectedPlacement(connection, client, args.placement, 'point', 'move');
368
+ mode = 'center';
369
+ centerOnX = rp.point.x;
370
+ centerOnY = rp.point.y;
371
+ placementInfo = { target: rp.target, anchors: rp.anchors, point: rp.point };
356
372
  }
357
- if (hasAbsolute && (args.absolute_x === undefined || args.absolute_y === undefined)) {
358
- throw new Error('Absolute mode requires both absolute_x and absolute_y.');
359
- }
360
- if (hasCenter && (args.center_on_x === undefined || args.center_on_y === undefined)) {
361
- throw new Error('Center mode requires both center_on_x and center_on_y.');
373
+ else {
374
+ const hasDelta = args.delta_x !== undefined || args.delta_y !== undefined;
375
+ const hasAbsolute = args.absolute_x !== undefined || args.absolute_y !== undefined;
376
+ const hasCenter = args.center_on_x !== undefined || args.center_on_y !== undefined;
377
+ const modeCount = (hasDelta ? 1 : 0) + (hasAbsolute ? 1 : 0) + (hasCenter ? 1 : 0);
378
+ if (modeCount === 0) {
379
+ throw new Error('No positioning mode specified. Pass a placement (anchors + a point relation), or exactly one pair: delta_x+delta_y (relative), absolute_x+absolute_y (target top-left), or center_on_x+center_on_y (target center).');
380
+ }
381
+ if (modeCount > 1) {
382
+ throw new Error('Mixed positioning modes. Pass exactly ONE pair: delta_x+delta_y OR absolute_x+absolute_y OR center_on_x+center_on_y.');
383
+ }
384
+ if (hasDelta && (args.delta_x === undefined || args.delta_y === undefined)) {
385
+ throw new Error('Delta mode requires both delta_x and delta_y.');
386
+ }
387
+ if (hasAbsolute && (args.absolute_x === undefined || args.absolute_y === undefined)) {
388
+ throw new Error('Absolute mode requires both absolute_x and absolute_y.');
389
+ }
390
+ if (hasCenter && (args.center_on_x === undefined || args.center_on_y === undefined)) {
391
+ throw new Error('Center mode requires both center_on_x and center_on_y.');
392
+ }
393
+ mode = hasAbsolute ? 'absolute' : hasCenter ? 'center' : 'delta';
394
+ deltaX = args.delta_x ?? 0;
395
+ deltaY = args.delta_y ?? 0;
396
+ absoluteX = args.absolute_x ?? 0;
397
+ absoluteY = args.absolute_y ?? 0;
398
+ centerOnX = args.center_on_x ?? 0;
399
+ centerOnY = args.center_on_y ?? 0;
362
400
  }
363
- const mode = hasAbsolute
364
- ? 'absolute'
365
- : hasCenter
366
- ? 'center'
367
- : 'delta';
368
- const deltaX = args.delta_x ?? 0;
369
- const deltaY = args.delta_y ?? 0;
370
- const absoluteX = args.absolute_x ?? 0;
371
- const absoluteY = args.absolute_y ?? 0;
372
- const centerOnX = args.center_on_x ?? 0;
373
- const centerOnY = args.center_on_y ?? 0;
374
401
  const script = await snippetClient.build('moveLayer', {
375
402
  deltaX,
376
403
  deltaY,
@@ -381,11 +408,13 @@ async function moveLayer(connection, snippetClient, rawArgs) {
381
408
  centerOnY,
382
409
  });
383
410
  const result = (await runScript(connection, script));
384
- const summary = mode === 'delta'
385
- ? `Layer moved by (${deltaX}, ${deltaY})px (relative)`
386
- : mode === 'absolute'
387
- ? `Layer top-left moved to (${absoluteX}, ${absoluteY})px — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`
388
- : `Layer center moved to (${centerOnX}, ${centerOnY})px — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`;
411
+ const summary = placementInfo
412
+ ? `Layer center moved to (${centerOnX}, ${centerOnY})px via placement (gate PASS) — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`
413
+ : mode === 'delta'
414
+ ? `Layer moved by (${deltaX}, ${deltaY})px (relative)`
415
+ : mode === 'absolute'
416
+ ? `Layer top-left moved to (${absoluteX}, ${absoluteY})px — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`
417
+ : `Layer center moved to (${centerOnX}, ${centerOnY})px — applied delta (${result.applied_delta_x ?? '?'}, ${result.applied_delta_y ?? '?'})`;
389
418
  const bgNote = result.background_promoted ? ' (background layer auto-promoted)' : '';
390
419
  return {
391
420
  content: [
@@ -394,7 +423,17 @@ async function moveLayer(connection, snippetClient, rawArgs) {
394
423
  text: `${summary}${bgNote}\nResult: ${JSON.stringify(result)}`,
395
424
  },
396
425
  ],
397
- structuredContent: result,
426
+ structuredContent: placementInfo
427
+ ? {
428
+ ...result,
429
+ placement: {
430
+ target: placementInfo.target,
431
+ gate: { pass: true },
432
+ anchors: placementInfo.anchors,
433
+ geometry: placementInfo.point,
434
+ },
435
+ }
436
+ : result,
398
437
  };
399
438
  }
400
439
  catch (error) {
@@ -9,26 +9,26 @@ already know which tool fits.
9
9
 
10
10
  If you reference a tool by name that isn't in your \`tools/list\`,
11
11
  work around it — don't speculate that it's there. The discovery chain
12
- is \`photoshop_ping\` (liveness) → this overview (workflow) →
13
- \`photoshop_list_capabilities\` (a compact, live map of what exists,
12
+ is \`ps_ping\` (liveness) → this overview (workflow) →
13
+ \`ps_list_capabilities\` (a compact, live map of what exists,
14
14
  grouped by capability) → \`tools/list\` (full schemas).
15
15
 
16
16
  ## The workflow contract
17
17
 
18
18
  Every non-trivial edit follows the same six phases:
19
19
 
20
- 1. **Assess** — \`photoshop_inspect\` (what=metadata / layer_tree / history /
21
- selection_info) + \`photoshop_get_preview\` to understand what you're working with.
20
+ 1. **Assess** — \`ps_inspect\` (what=metadata / layer_tree / history /
21
+ selection_info) + \`ps_get_preview\` to understand what you're working with.
22
22
  2. **Plan** — name the intent, the layers/regions involved, and the
23
23
  exit criteria. Tell the user before you execute.
24
24
  3. **Enact** — call the tools.
25
- 4. **Check** — \`photoshop_get_preview\` after every load-bearing change.
25
+ 4. **Check** — \`ps_get_preview\` after every load-bearing change.
26
26
  For spatial questions ("is this aligned?"), use annotations or
27
- \`photoshop_get_layer_bounds_diff\` — see Verification below.
27
+ \`ps_get_layer_bounds_diff\` — see Verification below.
28
28
  5. **Iterate** — up to 3 correction loops. If the third attempt still
29
29
  misses, surface the gap to the user instead of looping further.
30
- 6. **Finish** — \`photoshop_export\` (format jpeg/png) /
31
- \`photoshop_save_psd\`. Confirm the user wants to flatten before
30
+ 6. **Finish** — \`ps_export\` (format jpeg/png) /
31
+ \`ps_save_psd\`. Confirm the user wants to flatten before
32
32
  destructive saves.
33
33
 
34
34
  ## Capabilities map
@@ -36,61 +36,61 @@ Every non-trivial edit follows the same six phases:
36
36
  This map covers the workflow categories. For specific tool names and
37
37
  schemas in this session, consult \`tools/list\`.
38
38
 
39
- - **Adjustments (non-destructive)** — \`photoshop_add_adjustment_layer\`
39
+ - **Adjustments (non-destructive)** — \`ps_add_adjustment_layer\`
40
40
  covers brightness/contrast, levels, curves, hue/sat, color balance,
41
41
  photo filter, vibrance, channel mixer, selective color, black & white,
42
42
  gradient map, exposure, color lookup, invert. Always prefer this over
43
43
  destructive filters for tone/color work.
44
- - **Filters (destructive)** — \`photoshop_apply_filter\` with a
44
+ - **Filters (destructive)** — \`ps_apply_filter\` with a
45
45
  \`type\` (gaussian_blur, motion_blur, lens_blur, radial_blur, sharpen,
46
46
  smart_sharpen, noise, reduce_noise, high_pass, pixelate, distort,
47
47
  displace, oil_paint). Auto-duplicates the active layer by default so
48
48
  the original is preserved (\`target_was_copy: true\` in the response);
49
49
  pass \`apply_to_active_layer: true\` only when the user explicitly
50
50
  wants to bake into the original. Destructive tonal bakes with no
51
- adjustment-layer equivalent live in \`photoshop_apply_adjustment\`
51
+ adjustment-layer equivalent live in \`ps_apply_adjustment\`
52
52
  (type shadows_highlights / equalize / color_lookup).
53
53
  - **Layers** — create / delete / duplicate / select / rasterize /
54
- convert-to-smart-object / add layer style, plus \`photoshop_set_layer\`
54
+ convert-to-smart-object / add layer style, plus \`ps_set_layer\`
55
55
  (property opacity / blend_mode / visibility / locked / name),
56
- \`photoshop_merge\` (mode visible / stamp / flatten), and
57
- \`photoshop_transform_layer\` (op move / rotate / scale / fit / flip —
56
+ \`ps_merge\` (mode visible / stamp / flatten), and
57
+ \`ps_transform_layer\` (op move / rotate / scale / fit / flip —
58
58
  auto-promotes the background layer, so you don't have to duplicate
59
59
  first).
60
- - **Masks** — \`photoshop_layer_mask\` (op create / delete / apply).
60
+ - **Masks** — \`ps_layer_mask\` (op create / delete / apply).
61
61
  \`op: "create"\` automatically uses the current selection if one is
62
62
  active (reveals selection, hides outside); with no selection it
63
63
  creates a reveal-all mask. This is the right tool for "mask the placed
64
64
  image to the frame opening" — make the selection first, then call it.
65
65
  Don't write \`Mk Chnl At=Msk\` AM scripts for this — it's a one-call
66
- native. For VECTOR masks + work paths, see \`photoshop_vector_mask\`
67
- (op add / delete / link / unlink) and \`photoshop_path\`
66
+ native. For VECTOR masks + work paths, see \`ps_vector_mask\`
67
+ (op add / delete / link / unlink) and \`ps_path\`
68
68
  (selection↔path round-trip, stroke / fill / clip).
69
- - **Selection** — \`photoshop_select\` (mode rectangle / color_range /
69
+ - **Selection** — \`ps_select\` (mode rectangle / color_range /
70
70
  luminance_range / magic_wand / all / none / inverse),
71
- \`photoshop_modify_selection\` (op feather / refine_edge),
72
- \`photoshop_selection_channel\` (op save / load),
73
- \`photoshop_inspect\` (what=selection_info), \`photoshop_get_selection_preview\`.
71
+ \`ps_modify_selection\` (op feather / refine_edge),
72
+ \`ps_selection_channel\` (op save / load),
73
+ \`ps_inspect\` (what=selection_info), \`ps_get_selection_preview\`.
74
74
  Smart-selection (AI subject / sky) helpers may be available in
75
75
  \`tools/list\` — check there.
76
- - **Perception (on-device CV)** — \`photoshop_detect\` (faces +
77
- COCO-80 objects → real document-pixel boxes), \`photoshop_scene\`
76
+ - **Perception (on-device CV)** — \`ps_detect\` (faces +
77
+ COCO-80 objects → real document-pixel boxes), \`ps_read_scene\`
78
78
  (a structured read of subjects / horizon / tonal zones), and
79
- \`photoshop_select_by_reference\` (a NAMED target → a real pixel
79
+ \`ps_select_by_reference\` (a NAMED target → a real pixel
80
80
  selection instead of a guessed rectangle). The image is analyzed
81
81
  locally. Reach for these to get REAL coordinates rather than
82
82
  estimating from the preview — they're the antidote to spatial
83
83
  guessing.
84
- - **Text** — \`photoshop_create_text_layer\`, then \`photoshop_set_text\`
84
+ - **Text** — \`ps_create_text_layer\`, then \`ps_set_text\`
85
85
  (property font / color / alignment / content; font resolves family
86
86
  names to PostScript).
87
- - **Image** — \`photoshop_place_image\` (links external files as smart
88
- objects), \`photoshop_resize_image\`, \`photoshop_crop_document\`,
89
- \`photoshop_convert_image_mode\`, open / close / save_psd, and
90
- \`photoshop_export\` (format jpeg/png).
91
- - **Groups** — \`photoshop_create_group\`,
92
- \`photoshop_move_layer_to_group\`, \`photoshop_set_group_blend_mode\`,
93
- \`photoshop_ungroup\`, \`photoshop_delete_group\`.
87
+ - **Image** — \`ps_place_image\` (links external files as smart
88
+ objects), \`ps_resize_image\`, \`ps_crop_document\`,
89
+ \`ps_convert_image_mode\`, open / close / save_psd, and
90
+ \`ps_export\` (format jpeg/png).
91
+ - **Groups** — \`ps_create_group\`,
92
+ \`ps_move_layer_to_group\`, \`ps_set_group_blend_mode\`,
93
+ \`ps_ungroup\`, \`ps_delete_group\`.
94
94
  - **Verification** — see below; this is the most underused part of
95
95
  the surface.
96
96
  - **Templates** — when template tools are present in \`tools/list\`,
@@ -107,20 +107,20 @@ Vision-language models are bad at fine spatial estimation. You will
107
107
  hallucinate "looks good" when reviewing your own output. The fix is
108
108
  to convert spatial questions into semantic ones:
109
109
 
110
- - **\`photoshop_get_preview\`** with the \`annotations\` array — draw
110
+ - **\`ps_get_preview\`** with the \`annotations\` array — draw
111
111
  rectangles (by explicit bounds OR by layer name), guides, points,
112
112
  or selection markers ON the preview. "Is the placed image aligned
113
113
  with the frame?" becomes "Is the red wireframe touching the frame
114
114
  edges?" — a question you can answer reliably.
115
- - **\`photoshop_get_layer_bounds_diff\`** — pass a target rectangle
115
+ - **\`ps_get_layer_bounds_diff\`** — pass a target rectangle
116
116
  + a layer name + a tolerance. Returns a one-word verdict
117
117
  ("aligned" / "shifted right" / "layer too small") and numeric
118
118
  deltas. Use this when you need a yes/no on placement accuracy,
119
119
  not a visual judgment.
120
- - **\`photoshop_compare_regions\`** — per-channel histogram stats
120
+ - **\`ps_compare_regions\`** — per-channel histogram stats
121
121
  for two rectangles. Reach for this when you need a numeric diff
122
122
  (matched tones across two crops, before/after for an adjustment).
123
- - **\`photoshop_get_histogram\`** — full-image or per-channel. Reach
123
+ - **\`ps_get_histogram\`** — full-image or per-channel. Reach
124
124
  for this for clipping detection, exposure verification, or "is
125
125
  this image neutral-gray?" — \`get_preview\` + eyeballing doesn't
126
126
  surface clipping reliably.
@@ -134,12 +134,12 @@ fill black, for example. Re-read your inventory first.
134
134
 
135
135
  Common false escape-hatch reaches we want you to avoid:
136
136
  - "Add a layer mask from the current selection" →
137
- \`photoshop_layer_mask\` (op create) does this automatically when a
137
+ \`ps_layer_mask\` (op create) does this automatically when a
138
138
  selection is live.
139
139
  - "Move the layer so its center is at (x, y)" →
140
- \`photoshop_transform_layer\` (op move) with \`center_on_x\` /
140
+ \`ps_transform_layer\` (op move) with \`center_on_x\` /
141
141
  \`center_on_y\`.
142
- - "Rotate this background layer" → \`photoshop_transform_layer\`
142
+ - "Rotate this background layer" → \`ps_transform_layer\`
143
143
  (op rotate); it auto-promotes the background layer.
144
144
 
145
145
  If no typed tool fits AND the operation isn't a composition, surface
@@ -148,7 +148,7 @@ the gap to the user rather than fabricating a workaround.
148
148
  ## Known gaps
149
149
 
150
150
  - Frame opening / arbitrary edge detection — no general primitive.
151
- Best path: \`photoshop_select\` (mode magic_wand) inside the dark
151
+ Best path: \`ps_select\` (mode magic_wand) inside the dark
152
152
  interior + read the selection bounds via \`get_selection_info\`.
153
153
  - Smart Object editing — no native tool to enter / exit smart-object
154
154
  edit mode from outside the document. Plan around it.
@@ -159,7 +159,7 @@ the gap to the user rather than fabricating a workaround.
159
159
 
160
160
  If you've lost track of what's available — this brief scrolled out of
161
161
  context, or you're not sure which tool exists for a job — call
162
- \`photoshop_list_capabilities\`. It returns a compact, live map of every
162
+ \`ps_list_capabilities\`. It returns a compact, live map of every
163
163
  tool grouped by capability (each group's purpose + the tool names in
164
164
  it). It's cheaper than re-reading this overview and more current than
165
165
  any static list. Use it to find the right tool name, then \`tools/list\`
@@ -179,7 +179,7 @@ export function createOverviewTools(_connection, _snippetClient) {
179
179
  return [
180
180
  {
181
181
  tool: {
182
- name: 'photoshop_overview',
182
+ name: 'ps_overview',
183
183
  description: 'Orientation brief for the entire MCP — workflow contract, capabilities map by category, verification primitives, escape-hatch policy, known gaps. READ THIS FIRST when the user gives you an open-ended editing task. Read-only, idempotent, no document required, no Photoshop call (returns a static markdown brief). Skip it for trivial single-tool requests where you already know which tool fits.',
184
184
  inputSchema: overviewSchema,
185
185
  outputSchema: {
@@ -1,8 +1,11 @@
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 { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
5
+ import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
4
6
  const PATH_OPS = [
5
7
  'create_from_selection',
8
+ 'create_from_placement',
6
9
  'save',
7
10
  'list',
8
11
  'delete',
@@ -20,6 +23,7 @@ const pathInputSchema = {
20
23
  type: 'string',
21
24
  enum: [...PATH_OPS],
22
25
  description: 'create_from_selection: convert the active selection into a work path (tolerance = crude↔detailed dial; CLEARS the selection). ' +
26
+ 'create_from_placement: NAME a curve (placement → a path relation: `along` a traced edge / landmark curve, or a `segment` between anchors) and get an editable, saved vector path following it — the grounded pen; needs `name` (+ optional `closed`). ' +
23
27
  'save: persist the current work path under a name (required for stroke/clip-by-name and to survive the next create). ' +
24
28
  'list: inventory all paths (name, kind, subpath + anchor counts) — read-only. ' +
25
29
  'delete: remove a path (name → that saved path; omit name → the current work path). ' +
@@ -30,7 +34,16 @@ const pathInputSchema = {
30
34
  },
31
35
  name: {
32
36
  type: 'string',
33
- description: 'Path name. REQUIRED for save and set_clipping. Optional for delete / load_as_selection / stroke / fill — when omitted those target the current work path. Ignored by create_from_selection / list.',
37
+ description: 'Path name. REQUIRED for save, set_clipping, and create_from_placement (the new saved path is named). Optional for delete / load_as_selection / stroke / fill — when omitted those target the current work path. Ignored by create_from_selection / list.',
38
+ },
39
+ placement: {
40
+ ...PLACEMENT_SCHEMA,
41
+ description: 'create_from_placement only: NAME the curve the path follows, as an anchor-relational placement that resolves to a PATH (relation `along` a traced edge / a Pro landmark curve, or a `segment` between two anchors). The spatial-grounding resolver + objective gate produce AND verify the curve; the path is created ONLY if the gate PASSES. Name the curve — do NOT hand-type anchor points.',
42
+ },
43
+ closed: {
44
+ type: 'boolean',
45
+ default: false,
46
+ description: 'create_from_placement only: close the path into a loop (connect the last anchor back to the first). Default false (an open path).',
34
47
  },
35
48
  tolerance: {
36
49
  type: 'number',
@@ -59,7 +72,7 @@ const pathInputSchema = {
59
72
  tool: {
60
73
  type: 'string',
61
74
  enum: SUPPORTED_BRUSH_TOOLS,
62
- description: "stroke only: which brush-family tool paints the path. Default 'brush'. Same 16-tool set as photoshop_apply_brush_stroke.",
75
+ description: "stroke only: which brush-family tool paints the path. Default 'brush'. Same 16-tool set as ps_apply_brush_stroke.",
63
76
  default: 'brush',
64
77
  },
65
78
  color: {
@@ -98,12 +111,12 @@ const pathInputSchema = {
98
111
  },
99
112
  required: ['op'],
100
113
  };
101
- export function createPathTools(connection, snippetClient) {
114
+ export function createPathTools(connection, snippetClient, client = new OnnxLandmarkDetectionClient()) {
102
115
  return [
103
116
  {
104
117
  tool: {
105
- name: 'photoshop_path',
106
- description: 'Work with editable vector PATHS — the connective tissue between selections, brush strokes, and clipping. One op-discriminated tool: `create_from_selection` (selection → work path), `save` (name the work path), `list`, `delete`, `load_as_selection` (path → selection), `stroke` (paint a brush-family tool along a path), `fill` (fill a path region), `set_clipping` (mark a saved path as the clipping path). **Reach for this when**: turning a precise selection into a reusable/strokeable outline, converting a saved path back to a selection, or outlining/filling a shape exactly. create_from_selection and load_as_selection are an exact round-trip. stroke/fill bake pixels and auto-duplicate the target layer (Bundle O). Every op returns path_info (counts + per-path subpath/anchor totals) so you can verify the path landed; load_as_selection also returns selection_info.',
118
+ name: 'ps_path',
119
+ description: 'Work with editable vector PATHS — the connective tissue between selections, brush strokes, and clipping. One op-discriminated tool: `create_from_selection` (selection → work path), `create_from_placement` (NAME a curve → editable saved path, the grounded pen), `save` (name the work path), `list`, `delete`, `load_as_selection` (path → selection), `stroke` (paint a brush-family tool along a path), `fill` (fill a path region), `set_clipping` (mark a saved path as the clipping path). **Reach for this when**: tracing a named curve (an edge, a landmark contour) into an editable path, turning a precise selection into a reusable/strokeable outline, converting a saved path back to a selection, or outlining/filling a shape exactly. create_from_selection and load_as_selection are an exact round-trip. stroke/fill bake pixels and auto-duplicate the target layer (Bundle O). Every op returns path_info (counts + per-path subpath/anchor totals) so you can verify the path landed; load_as_selection also returns selection_info.',
107
120
  inputSchema: pathInputSchema,
108
121
  outputSchema: {
109
122
  type: 'object',
@@ -118,6 +131,9 @@ export function createPathTools(connection, snippetClient) {
118
131
  name: { type: 'string' },
119
132
  path_name: { type: 'string' },
120
133
  tolerance: { type: 'number' },
134
+ anchors: { type: 'number' },
135
+ closed: { type: 'boolean' },
136
+ placement: { type: 'object' },
121
137
  selection_consumed: { type: 'boolean' },
122
138
  operation: { type: 'string' },
123
139
  tool: { type: 'string' },
@@ -139,22 +155,32 @@ export function createPathTools(connection, snippetClient) {
139
155
  idempotentHint: false,
140
156
  },
141
157
  },
142
- handler: async (args) => runPathOp(connection, snippetClient, args),
158
+ handler: async (args) => runPathOp(connection, snippetClient, client, args),
143
159
  },
144
160
  ];
145
161
  }
146
- async function runPathOp(connection, snippetClient, rawArgs) {
162
+ async function runPathOp(connection, snippetClient, client, rawArgs) {
147
163
  try {
148
164
  const args = validateArgs(pathInputSchema, rawArgs);
149
165
  const op = args.op;
150
166
  const name = args.name;
151
167
  let snippet;
152
168
  let params;
169
+ let placementInfo;
153
170
  switch (op) {
154
171
  case 'create_from_selection':
155
172
  snippet = 'createPathFromSelection';
156
173
  params = { tolerance: args.tolerance };
157
174
  break;
175
+ case 'create_from_placement': {
176
+ if (!name)
177
+ return errorResult('create_from_placement requires a "name" for the new saved path (pathItems.add needs one).');
178
+ const rp = await resolveExpectedPlacement(connection, client, args.placement, 'path', 'path');
179
+ snippet = 'createPathFromPoints';
180
+ params = { name, points: rp.curve, closed: args.closed ?? false };
181
+ placementInfo = { curve: rp.curve, anchors: rp.anchors, summary: rp.summary };
182
+ break;
183
+ }
158
184
  case 'save':
159
185
  if (!name)
160
186
  return missingName('save');
@@ -219,8 +245,20 @@ async function runPathOp(connection, snippetClient, rawArgs) {
219
245
  }
220
246
  const script = await snippetClient.build(snippet, params);
221
247
  const result = (await runScript(connection, script));
248
+ if (placementInfo) {
249
+ result.placement = {
250
+ target: 'path',
251
+ gate: { pass: true },
252
+ anchors: placementInfo.anchors,
253
+ points: placementInfo.curve.length,
254
+ summary: placementInfo.summary,
255
+ };
256
+ }
257
+ const text = placementInfo
258
+ ? `${summarize(op, result)} Path follows the resolved ${placementInfo.summary} (placement gate PASS).`
259
+ : summarize(op, result);
222
260
  return {
223
- content: [{ type: 'text', text: summarize(op, result) }],
261
+ content: [{ type: 'text', text }],
224
262
  structuredContent: result,
225
263
  };
226
264
  }
@@ -229,7 +267,7 @@ async function runPathOp(connection, snippetClient, rawArgs) {
229
267
  content: [
230
268
  {
231
269
  type: 'text',
232
- text: `Error in photoshop_path: ${error instanceof Error ? error.message : String(error)}`,
270
+ text: `Error in ps_path: ${error instanceof Error ? error.message : String(error)}`,
233
271
  },
234
272
  ],
235
273
  isError: true,
@@ -241,12 +279,15 @@ function missingName(op) {
241
279
  content: [
242
280
  {
243
281
  type: 'text',
244
- text: `photoshop_path op=${op} requires a "name" (the saved path to act on). Use op=save to name the current work path first.`,
282
+ text: `ps_path op=${op} requires a "name" (the saved path to act on). Use op=save to name the current work path first.`,
245
283
  },
246
284
  ],
247
285
  isError: true,
248
286
  };
249
287
  }
288
+ function errorResult(msg) {
289
+ return { content: [{ type: 'text', text: `ps_path: ${msg}` }], isError: true };
290
+ }
250
291
  function summarize(op, r) {
251
292
  const info = r.path_info;
252
293
  const pathCount = info?.count ?? r.count;
@@ -254,6 +295,8 @@ function summarize(op, r) {
254
295
  switch (op) {
255
296
  case 'create_from_selection':
256
297
  return `Created a work path from the selection (tolerance ${r.tolerance}); the selection was consumed.${suffix}`;
298
+ case 'create_from_placement':
299
+ return `Created editable path "${r.name}" from the named curve (${r.anchors} anchors${r.closed ? ', closed' : ''}).${suffix}`;
257
300
  case 'save':
258
301
  return `Saved path "${r.name}".${suffix}`;
259
302
  case 'list':
@@ -112,7 +112,7 @@ async function runPortraitOp(connection, snippet, client, rawArgs) {
112
112
  catch (error) {
113
113
  const msg = error instanceof Error ? error.message : String(error);
114
114
  return {
115
- content: [{ type: 'text', text: `Error in photoshop_portrait_touchup: ${msg}` }],
115
+ content: [{ type: 'text', text: `Error in ps_portrait_touchup: ${msg}` }],
116
116
  isError: true,
117
117
  };
118
118
  }
@@ -121,8 +121,8 @@ export function createPortraitTools(connection, snippetClient, client = new Onnx
121
121
  return [
122
122
  {
123
123
  tool: {
124
- name: 'photoshop_portrait_touchup',
125
- description: 'Detection-driven face touch-ups using LOCAL on-device face detection. op=dodge_face brightens the detected face(s) with a non-destructive masked adjustment layer. op=soften_skin applies a soft blur over the face(s) on a duplicate layer (light, bounding-box-level smoothing). Works from the face bounding box — precise landmark ops (eyes/teeth/lips) are a Pro feature. Multiple faces are handled together. Pair with photoshop_detect (target=faces) to preview detection first.',
124
+ name: 'ps_portrait_touchup',
125
+ description: 'Detection-driven face touch-ups using LOCAL on-device face detection. op=dodge_face brightens the detected face(s) with a non-destructive masked adjustment layer. op=soften_skin applies a soft blur over the face(s) on a duplicate layer (light, bounding-box-level smoothing). Works from the face bounding box — precise landmark ops (eyes/teeth/lips) are a Pro feature. Multiple faces are handled together. Pair with ps_detect (target=faces) to preview detection first.',
126
126
  inputSchema: portraitSchema,
127
127
  outputSchema: {
128
128
  type: 'object',