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
@@ -0,0 +1,241 @@
1
+ import { runScript } from '../utils/run-script.js';
2
+ import { validateArgs } from '../utils/validate.js';
3
+ const BLEND_MODES = [
4
+ 'normal',
5
+ 'multiply',
6
+ 'screen',
7
+ 'overlay',
8
+ 'darken',
9
+ 'lighten',
10
+ 'soft_light',
11
+ 'hard_light',
12
+ 'difference',
13
+ 'exclusion',
14
+ 'subtract',
15
+ 'add',
16
+ ];
17
+ const blendFragment = {
18
+ type: 'string',
19
+ enum: [...BLEND_MODES],
20
+ description: 'Calculation blend mode: how the source combines with the target. multiply (darken/texture), screen (lighten), overlay/soft_light/hard_light (contrast), difference/exclusion/subtract/add (channel math for masks).',
21
+ };
22
+ const applyImageSchema = {
23
+ type: 'object',
24
+ properties: {
25
+ source_layer: {
26
+ type: 'string',
27
+ description: "Which layer to pull the source from: 'merged' (the flattened composite, default) or an exact layer name.",
28
+ default: 'merged',
29
+ },
30
+ source_channel: {
31
+ type: 'string',
32
+ enum: ['rgb', 'red', 'green', 'blue', 'alpha'],
33
+ description: "Source channel: 'rgb' (the composite, default), a single 'red'/'green'/'blue' channel, or 'alpha' (then set source_alpha_name).",
34
+ default: 'rgb',
35
+ },
36
+ source_alpha_name: {
37
+ type: 'string',
38
+ description: "Required when source_channel='alpha': the name of the alpha channel to read.",
39
+ },
40
+ blend: blendFragment,
41
+ opacity: {
42
+ type: 'integer',
43
+ description: 'Blend opacity 0-100. Default 100 (full strength).',
44
+ minimum: 0,
45
+ maximum: 100,
46
+ default: 100,
47
+ },
48
+ apply_to_active_layer: {
49
+ type: 'boolean',
50
+ description: 'If false (default) the composite is baked onto a DUPLICATE of the active layer (original preserved — delete the copy to revert). Set true to bake destructively in place.',
51
+ default: false,
52
+ },
53
+ },
54
+ required: ['blend'],
55
+ };
56
+ const calculationsSchema = {
57
+ type: 'object',
58
+ properties: {
59
+ source1_layer: {
60
+ type: 'string',
61
+ description: "Source 1 layer: 'merged' (default) or an exact layer name.",
62
+ default: 'merged',
63
+ },
64
+ source1_channel: {
65
+ type: 'string',
66
+ enum: ['red', 'green', 'blue', 'alpha'],
67
+ description: "Source 1 channel: a single 'red'/'green'/'blue' channel, or 'alpha' (+ name).",
68
+ default: 'red',
69
+ },
70
+ source1_alpha_name: {
71
+ type: 'string',
72
+ description: "Required when source1_channel='alpha'.",
73
+ },
74
+ source2_layer: {
75
+ type: 'string',
76
+ description: "Source 2 layer: 'merged' (default) or an exact layer name.",
77
+ default: 'merged',
78
+ },
79
+ source2_channel: {
80
+ type: 'string',
81
+ enum: ['red', 'green', 'blue', 'alpha'],
82
+ description: "Source 2 channel: a single 'red'/'green'/'blue' channel, or 'alpha' (+ name).",
83
+ default: 'red',
84
+ },
85
+ source2_alpha_name: {
86
+ type: 'string',
87
+ description: "Required when source2_channel='alpha'.",
88
+ },
89
+ blend: blendFragment,
90
+ opacity: {
91
+ type: 'integer',
92
+ description: 'Blend opacity 0-100. Default 100.',
93
+ minimum: 0,
94
+ maximum: 100,
95
+ default: 100,
96
+ },
97
+ },
98
+ required: ['blend'],
99
+ };
100
+ export function createChannelComposeTools(connection, snippetClient) {
101
+ return [
102
+ {
103
+ tool: {
104
+ name: 'ps_apply_image',
105
+ description: 'Composite a source layer + channel onto the ACTIVE layer using a calculation blend mode (Image ▸ Apply Image). **Reach for this** for luminosity blends, frequency-separation re-blends, texture overlays, or pulling one channel into another. Source = a layer (merged or named) and a channel (rgb/red/green/blue/alpha). Bakes into pixels, so by default it runs on a DUPLICATE of the active layer (set apply_to_active_layer=true to bake in place). The active layer must be a normal pixel layer. (AM-only; verified live on PS 27.2.0.)',
106
+ inputSchema: applyImageSchema,
107
+ outputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ applied: { type: 'boolean' },
111
+ source_channel: { type: 'string' },
112
+ source_layer: { type: 'string' },
113
+ blend: { type: 'string' },
114
+ opacity: { type: 'number' },
115
+ target_was_copy: { type: 'boolean' },
116
+ target_layer_name: { type: 'string' },
117
+ context: { type: 'object' },
118
+ },
119
+ },
120
+ annotations: {
121
+ title: 'Apply Image',
122
+ destructiveHint: true,
123
+ idempotentHint: false,
124
+ },
125
+ },
126
+ handler: async (args) => applyImage(connection, snippetClient, args),
127
+ },
128
+ {
129
+ tool: {
130
+ name: 'ps_calculations',
131
+ description: 'Blend TWO sources (each a layer + single channel) into a NEW alpha channel via a calculation blend mode (Image ▸ Calculations). **Reach for this** to build advanced selections/masks from channel math — e.g. difference of two channels to isolate edges, or multiply two channels for a luminosity mask. Non-destructive: adds a new channel (delete it to revert); the result is loadable as a selection via ps_selection_channel op=load. (AM-only; verified live on PS 27.2.0.)',
132
+ inputSchema: calculationsSchema,
133
+ outputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ calculated: { type: 'boolean' },
137
+ new_channel_name: { type: 'string' },
138
+ channel_count: { type: 'number' },
139
+ blend: { type: 'string' },
140
+ opacity: { type: 'number' },
141
+ context: { type: 'object' },
142
+ },
143
+ },
144
+ annotations: {
145
+ title: 'Calculations',
146
+ destructiveHint: false,
147
+ idempotentHint: false,
148
+ },
149
+ },
150
+ handler: async (args) => calculations(connection, snippetClient, args),
151
+ },
152
+ ];
153
+ }
154
+ async function applyImage(connection, snippetClient, rawArgs) {
155
+ try {
156
+ const args = validateArgs(applyImageSchema, rawArgs);
157
+ const params = {
158
+ sourceLayer: args.source_layer ?? 'merged',
159
+ sourceChannel: args.source_channel ?? 'rgb',
160
+ blend: args.blend,
161
+ opacity: args.opacity ?? 100,
162
+ applyToActiveLayer: args.apply_to_active_layer ?? false,
163
+ };
164
+ if (args.source_alpha_name !== undefined)
165
+ params.sourceAlphaName = args.source_alpha_name;
166
+ if (params.sourceChannel === 'alpha' && !params.sourceAlphaName) {
167
+ throw new Error("source_channel='alpha' requires source_alpha_name");
168
+ }
169
+ const script = await snippetClient.build('applyImage', params);
170
+ const result = (await runScript(connection, script));
171
+ result.source_channel =
172
+ params.sourceChannel === 'alpha' ? params.sourceAlphaName : params.sourceChannel;
173
+ result.blend = params.blend;
174
+ return {
175
+ content: [
176
+ {
177
+ type: 'text',
178
+ text: `Apply Image: ${String(result.source_channel ?? '')} of "${String(result.source_layer ?? '')}" (${String(result.blend ?? '')} @ ${String(result.opacity ?? '')}%) onto "${String(result.target_layer_name ?? '')}".`,
179
+ },
180
+ ],
181
+ structuredContent: result,
182
+ };
183
+ }
184
+ catch (error) {
185
+ return {
186
+ content: [
187
+ {
188
+ type: 'text',
189
+ text: `Error in ps_apply_image: ${error instanceof Error ? error.message : String(error)}`,
190
+ },
191
+ ],
192
+ isError: true,
193
+ };
194
+ }
195
+ }
196
+ async function calculations(connection, snippetClient, rawArgs) {
197
+ try {
198
+ const args = validateArgs(calculationsSchema, rawArgs);
199
+ const params = {
200
+ source1Layer: args.source1_layer ?? 'merged',
201
+ source1Channel: args.source1_channel ?? 'red',
202
+ source2Layer: args.source2_layer ?? 'merged',
203
+ source2Channel: args.source2_channel ?? 'red',
204
+ blend: args.blend,
205
+ opacity: args.opacity ?? 100,
206
+ };
207
+ if (args.source1_alpha_name !== undefined)
208
+ params.source1AlphaName = args.source1_alpha_name;
209
+ if (args.source2_alpha_name !== undefined)
210
+ params.source2AlphaName = args.source2_alpha_name;
211
+ if (params.source1Channel === 'alpha' && !params.source1AlphaName) {
212
+ throw new Error("source1_channel='alpha' requires source1_alpha_name");
213
+ }
214
+ if (params.source2Channel === 'alpha' && !params.source2AlphaName) {
215
+ throw new Error("source2_channel='alpha' requires source2_alpha_name");
216
+ }
217
+ const script = await snippetClient.build('calculations', params);
218
+ const result = (await runScript(connection, script));
219
+ result.blend = params.blend;
220
+ return {
221
+ content: [
222
+ {
223
+ type: 'text',
224
+ text: `Calculations → new channel "${String(result.new_channel_name ?? '')}" (${String(result.blend ?? '')} @ ${String(result.opacity ?? '')}%).`,
225
+ },
226
+ ],
227
+ structuredContent: result,
228
+ };
229
+ }
230
+ catch (error) {
231
+ return {
232
+ content: [
233
+ {
234
+ type: 'text',
235
+ text: `Error in ps_calculations: ${error instanceof Error ? error.message : String(error)}`,
236
+ },
237
+ ],
238
+ isError: true,
239
+ };
240
+ }
241
+ }
@@ -161,7 +161,7 @@ export function createDetectionTools(connection, _snippetClient, client = new On
161
161
  return [
162
162
  {
163
163
  tool: {
164
- name: 'photoshop_detect',
164
+ name: 'ps_detect',
165
165
  description: 'Locate faces and/or COCO-80 objects (person, dog, car, chair, sofa, …) in the active document using LOCAL on-device computer vision — the image is never sent anywhere. Returns labeled bounding boxes in DOCUMENT-pixel space plus an annotated preview for visual confirmation. Use this to gain semantic scene awareness ("there are 2 people and a dog") and real coordinates before any spatially-targeted edit — far more reliable than estimating positions from a preview. `target` selects faces / objects / both. Read-only: renders a throwaway duplicate, never modifies the working document. Boxes are [x1, y1, x2, y2]. Validate surprising results against the annotated image before acting on them.',
166
166
  inputSchema: detectSchema,
167
167
  outputSchema: {
@@ -0,0 +1,66 @@
1
+ import { collectDiagnostics, writeDiagnosticBundle, ISSUES_URL } from '../diagnostics/collect.js';
2
+ const reportSchema = {
3
+ type: 'object',
4
+ properties: {
5
+ note: {
6
+ type: 'string',
7
+ description: 'Optional short description of the problem (what went wrong, what you were doing). Embedded verbatim after sanitization. No file contents or paths needed.',
8
+ },
9
+ },
10
+ };
11
+ export function createDiagnosticsTools(_connection, _snippetClient, deps = {}) {
12
+ const collect = deps.collect ?? collectDiagnostics;
13
+ const writeBundle = deps.write ?? writeDiagnosticBundle;
14
+ return [
15
+ {
16
+ tool: {
17
+ name: 'ps_report_problem',
18
+ description: "Collect an ANONYMIZED diagnostic bundle and write it to the user's Downloads folder so they can attach it to a bug report — use when Editmamei misbehaves (won't connect, a tool keeps failing, unexpected results). The bundle holds recent server logs, system info (Editmamei/OS/Photoshop versions), and a content-free summary of recent tool calls (name, success, duration, error class). It contains NO image content, NO tool arguments, and file paths reduced to basenames. Does not touch Photoshop; writes one JSON file. After calling, tell the user the file path and that they can attach it to a new issue at " +
19
+ ISSUES_URL +
20
+ '.',
21
+ inputSchema: reportSchema,
22
+ outputSchema: {
23
+ type: 'object',
24
+ properties: {
25
+ path: { type: 'string' },
26
+ bytes: { type: 'number' },
27
+ server_log_lines: { type: 'number' },
28
+ recent_session_count: { type: 'number' },
29
+ desktop_log_included: { type: 'boolean' },
30
+ issues_url: { type: 'string' },
31
+ },
32
+ },
33
+ annotations: {
34
+ title: 'Report a problem',
35
+ readOnlyHint: false,
36
+ destructiveHint: false,
37
+ idempotentHint: false,
38
+ openWorldHint: true,
39
+ },
40
+ },
41
+ handler: async (args) => {
42
+ const note = typeof args.note === 'string' ? args.note : undefined;
43
+ const bundle = await collect({ note });
44
+ const { path, bytes } = await writeBundle(bundle);
45
+ return {
46
+ content: [
47
+ {
48
+ type: 'text',
49
+ text: `Wrote an anonymized diagnostic bundle to:\n ${path}\n\n` +
50
+ `It contains recent logs + system info — no images, no full file paths, no tool arguments. ` +
51
+ `Attach this file to a new issue at ${ISSUES_URL} so the maintainers can debug.`,
52
+ },
53
+ ],
54
+ structuredContent: {
55
+ path,
56
+ bytes,
57
+ server_log_lines: bundle.server_log.length,
58
+ recent_session_count: bundle.recent_sessions.length,
59
+ desktop_log_included: bundle.desktop_log.length > 0,
60
+ issues_url: ISSUES_URL,
61
+ },
62
+ };
63
+ },
64
+ },
65
+ ];
66
+ }
@@ -152,8 +152,8 @@ export function createDocumentTools(connection, snippetClient) {
152
152
  return [
153
153
  {
154
154
  tool: {
155
- name: 'photoshop_create_document',
156
- description: 'Create a new empty Photoshop document with given dimensions, resolution, and color mode. Returns the new document id and name plus active context. Use this when starting from scratch; prefer photoshop_open_document to load an existing file.',
155
+ name: 'ps_create_document',
156
+ description: 'Create a new empty Photoshop document with given dimensions, resolution, and color mode. Returns the new document id and name plus active context. Use this when starting from scratch; prefer ps_open_document to load an existing file.',
157
157
  inputSchema: createDocumentSchema,
158
158
  outputSchema: {
159
159
  type: 'object',
@@ -172,7 +172,7 @@ export function createDocumentTools(connection, snippetClient) {
172
172
  },
173
173
  {
174
174
  tool: {
175
- name: 'photoshop_close_document',
175
+ name: 'ps_close_document',
176
176
  description: 'Close the active Photoshop document. Destructive if save=false and the document has unsaved changes. Returns the closed document name plus a fresh context block (which document, if any, is active afterwards).',
177
177
  inputSchema: closeDocumentSchema,
178
178
  outputSchema: {
@@ -194,7 +194,7 @@ export function createDocumentTools(connection, snippetClient) {
194
194
  },
195
195
  {
196
196
  tool: {
197
- name: 'photoshop_open_document',
197
+ name: 'ps_open_document',
198
198
  description: 'Open a file from disk into Photoshop with all dialogs suppressed (uses last-used Camera Raw settings for raw/HEIC). Returns document name, dimensions, color mode, and whether the source was a raw format. Use this in the pipeline to load Inbox files for editing.',
199
199
  inputSchema: openDocumentSchema,
200
200
  outputSchema: {
@@ -223,7 +223,7 @@ export function createDocumentTools(connection, snippetClient) {
223
223
  },
224
224
  {
225
225
  tool: {
226
- name: 'photoshop_save_psd',
226
+ name: 'ps_save_psd',
227
227
  description: 'Save the active document as a layered PSD to the given path. Saves as a copy, so the working document is unmodified and unrenamed. Use this in the pipeline to land an editable PSD into the Edit folder.',
228
228
  inputSchema: savePsdSchema,
229
229
  outputSchema: {
@@ -248,7 +248,7 @@ export function createDocumentTools(connection, snippetClient) {
248
248
  },
249
249
  {
250
250
  tool: {
251
- name: 'photoshop_export',
251
+ name: 'ps_export',
252
252
  description: 'Export the active document to a flattened image file — JPEG or PNG, chosen via `format`. Operates on a duplicate, so the working document is unchanged. Optionally downscales so the long edge equals long_edge_px. JPEG: quality 0-100 (Save-As scale), optional sRGB convert + profile embed. PNG: transparent background (alpha preserved) or flattened onto white, compression 0-9.',
253
253
  inputSchema: exportInputSchema,
254
254
  outputSchema: {