editmamei 1.0.3 → 1.2.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 (44) hide show
  1. package/README.md +9 -9
  2. package/dist/api/extendscript/_helpers.js +70 -2
  3. package/dist/api/photoshop-api.js +17 -2
  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/activate.js +1 -1
  8. package/dist/cli/config.js +1 -1
  9. package/dist/cli/deactivate.js +1 -1
  10. package/dist/core/raw-develop-state.js +13 -0
  11. package/dist/core/server.js +147 -18
  12. package/dist/core/tool-groups.js +18 -21
  13. package/dist/core/tool-tiers.js +7 -10
  14. package/dist/detection/runtime.js +2 -2
  15. package/dist/modules/ce/index.js +2 -0
  16. package/dist/perception/region-scorer.js +1 -1
  17. package/dist/platform/connection.js +15 -0
  18. package/dist/skills/editmamei-skill.zip +0 -0
  19. package/dist/telemetry/client.js +15 -2
  20. package/dist/telemetry/sanitize.js +1 -0
  21. package/dist/tools/adjustment-tools.js +21 -3
  22. package/dist/tools/detection-tools.js +1 -1
  23. package/dist/tools/document-tools.js +5 -2
  24. package/dist/tools/filter-tools.js +118 -15
  25. package/dist/tools/group-tools.js +146 -146
  26. package/dist/tools/history-tools.js +1 -1
  27. package/dist/tools/inspect-tools.js +22 -3
  28. package/dist/tools/layer-tools.js +4 -27
  29. package/dist/tools/overview-tools.js +37 -8
  30. package/dist/tools/preview-tools.js +3 -2
  31. package/dist/tools/scene-tools.js +1 -1
  32. package/dist/tools/selection-tools.js +91 -18
  33. package/dist/tools/sky-tools.js +126 -0
  34. package/dist/tools/smart-object-tools.js +163 -0
  35. package/dist/tools/text-tools.js +62 -41
  36. package/dist/update/check.js +65 -6
  37. package/dist/update/session-fixes.js +33 -0
  38. package/dist/utils/jsx.js +2 -1
  39. package/dist/utils/operation-timeouts.js +2 -0
  40. package/dist/utils/session-log.js +46 -6
  41. package/dist/utils/temp.js +1 -1
  42. package/dist/utils/tool-helpers.js +23 -0
  43. package/dist/version.js +1 -1
  44. package/package.json +13 -2
@@ -1,11 +1,7 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { GROUP_BLEND_MODES } from '../utils/blend-modes.js';
3
3
  import { validateArgs } from '../utils/validate.js';
4
- import { toolErrorResult, runSnippetTool } from '../utils/tool-helpers.js';
5
- const emptySchema = {
6
- type: 'object',
7
- properties: {},
8
- };
4
+ import { toolErrorResult, runSnippetTool, unknownDiscriminator } from '../utils/tool-helpers.js';
9
5
  const deleteGroupSchema = {
10
6
  type: 'object',
11
7
  properties: {
@@ -45,7 +41,7 @@ const moveLayerToGroupSchema = {
45
41
  properties: {
46
42
  layer_name: {
47
43
  type: 'string',
48
- description: 'Name of the layer to move (recursive search).',
44
+ description: 'Name of the layer to move (recursive search). A layer is preferred over a group of the same name; a group is moved only when no layer matches, which is how one group is nested inside another.',
49
45
  },
50
46
  group_name: {
51
47
  type: 'string',
@@ -83,189 +79,193 @@ const ungroupSchema = {
83
79
  },
84
80
  required: ['name', 'confirm'],
85
81
  };
82
+ const CLIPPING_MASK_OPS = ['create', 'release'];
83
+ const clippingMaskSchema = {
84
+ type: 'object',
85
+ properties: {
86
+ op: {
87
+ type: 'string',
88
+ enum: [...CLIPPING_MASK_OPS],
89
+ description: 'create: clip the active layer to the layer directly below it (that layer becomes the alpha source); no-ops (already_clipped:true) when the layer is already clipped. ' +
90
+ 'release: release the active layer from its clipping mask; no-ops (released:false) when the layer is not clipped.',
91
+ },
92
+ },
93
+ required: ['op'],
94
+ };
95
+ const clippingMaskOpArgsSchema = {
96
+ type: 'object',
97
+ properties: {},
98
+ };
99
+ const GROUP_OPS = ['create', 'delete', 'ungroup', 'add_layer', 'set_blend_mode'];
100
+ export const GROUP_OP_SCHEMAS = {
101
+ create: createGroupSchema,
102
+ delete: deleteGroupSchema,
103
+ ungroup: ungroupSchema,
104
+ add_layer: moveLayerToGroupSchema,
105
+ set_blend_mode: setGroupBlendModeSchema,
106
+ };
107
+ const GROUP_INPUT_SCHEMA = {
108
+ type: 'object',
109
+ properties: {
110
+ op: {
111
+ type: 'string',
112
+ enum: [...GROUP_OPS],
113
+ description: "create: make a new group above the active layer named `name` (hoisted out of an active group by default; into_active_group:true keeps Photoshop's native nesting), optionally moving `layers` into it. " +
114
+ 'delete: DESTRUCTIVE — delete group `name` and everything inside it (nested groups and their layers); requires confirm:true. To dissolve a group while keeping its contents, use ungroup instead. ' +
115
+ 'ungroup: DESTRUCTIVE structural change — dissolve group `name`, promoting its contents to the parent level in their existing stack order; requires confirm:true. ' +
116
+ 'add_layer: move `layer_name` into `group_name` (top of its stack). ' +
117
+ "set_blend_mode: set group `name`'s blend mode to `blend_mode` — PASSTHROUGH (default for new groups) lets adjustments inside affect layers below the group; NORMAL treats the group as a single composite.",
118
+ },
119
+ ...createGroupSchema.properties,
120
+ name: {
121
+ type: 'string',
122
+ description: 'Group name. create: name for the NEW group. delete/ungroup/set_blend_mode: the EXISTING group to act on (recursive search).',
123
+ },
124
+ confirm: {
125
+ type: 'boolean',
126
+ description: 'REQUIRED for op=delete and op=ungroup, ignored by the other ops. Must be true — guards against accidental loss of a group and (for delete) everything it contains.',
127
+ },
128
+ ...moveLayerToGroupSchema.properties,
129
+ blend_mode: setGroupBlendModeSchema.properties.blend_mode,
130
+ },
131
+ required: ['op'],
132
+ };
86
133
  export function createGroupTools(connection, snippetClient) {
87
134
  return [
88
135
  {
89
136
  tool: {
90
- name: 'ps_create_group',
91
- description: `Create a new layer group (LayerSet) above the active layer with the given name — hoisted out of the active layer's group by default even though Photoshop's own Mk-layerSection placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting; this is why groups created one after another land as siblings rather than nested, so bottom-to-top group creation is safe). Optionally moves existing layers into it in one step. Non-destructive. Foundational for structured non-destructive editing — e.g. group all adjustment layers into an "edits" group so you can A/B toggle the whole stack via group visibility.`,
92
- inputSchema: createGroupSchema,
137
+ name: 'ps_group',
138
+ description: 'Layer group (LayerSet) lifecycle and membership — choose the operation with `op`. create/delete/ungroup/add_layer/set_blend_mode. See the `op` enum for per-operation params. delete and ungroup are DESTRUCTIVE and require confirm:true — delete removes the group AND everything inside it; ungroup dissolves the group but promotes its contents to the parent level (use ungroup, not delete, to keep the layers).',
139
+ inputSchema: GROUP_INPUT_SCHEMA,
93
140
  outputSchema: {
94
141
  type: 'object',
95
142
  properties: {
96
- created: { type: 'boolean' },
143
+ created: { type: 'boolean', description: 'op=create: true on success.' },
144
+ deleted: { type: 'boolean', description: 'op=delete: true on success.' },
145
+ ungrouped: { type: 'boolean', description: 'op=ungroup: true on success.' },
146
+ moved: { type: 'boolean', description: 'op=add_layer: true on success.' },
147
+ set: { type: 'boolean', description: 'op=set_blend_mode: true on success.' },
97
148
  groupName: { type: 'string' },
98
- moved_count: { type: 'number' },
99
- not_found: { type: 'array', items: { type: 'string' } },
149
+ layerName: { type: 'string', description: 'op=add_layer: the moved layer.' },
150
+ blendMode: { type: 'string', description: 'op=set_blend_mode: the mode applied.' },
151
+ moved_count: { type: 'number', description: 'op=create: layers moved into it.' },
152
+ not_found: {
153
+ type: 'array',
154
+ items: { type: 'string' },
155
+ description: 'op=create: requested layer names that were not found.',
156
+ },
100
157
  hoisted: {
101
158
  type: 'boolean',
102
- description: 'True when the new group had to be moved back out of the previously-active group to honor into_active_group:false (the default). False when it landed correctly on its own, or when the move-back itself failed — check the layer tree if this matters and hoisted is false.',
159
+ description: 'op=create: true when the new group had to be moved back out of the previously-active group to honor into_active_group:false.',
103
160
  },
104
161
  parent_path: {
105
162
  type: ['array', 'null'],
106
163
  items: { type: 'string' },
107
- description: 'The containing-group name chain (outermost first), empty array at the document root.',
164
+ description: 'op=create: the containing-group name chain (outermost first), empty array at the document root.',
165
+ },
166
+ descendants_deleted: {
167
+ type: 'number',
168
+ description: 'op=delete: total layers removed with the group.',
169
+ },
170
+ children_promoted: {
171
+ type: 'number',
172
+ description: 'op=ungroup: children promoted to the parent level.',
173
+ },
174
+ child_names: {
175
+ type: 'array',
176
+ items: { type: 'string' },
177
+ description: 'op=ungroup: names of the promoted children.',
108
178
  },
109
179
  context: { type: 'object' },
110
180
  },
111
181
  },
112
182
  annotations: {
113
- title: 'Create Layer Group',
114
- idempotentHint: false,
115
- },
116
- },
117
- handler: async (args) => createGroup(connection, snippetClient, args),
118
- },
119
- {
120
- tool: {
121
- name: 'ps_move_layer_to_group',
122
- description: 'Move a named layer into a named group. The layer is placed at the top of the group stack. Both layer_name and group_name are looked up recursively. Throws if either is not found, or if the layer IS the group itself.',
123
- inputSchema: moveLayerToGroupSchema,
124
- outputSchema: {
125
- type: 'object',
126
- properties: {
127
- moved: { type: 'boolean' },
128
- layerName: { type: 'string' },
129
- groupName: { type: 'string' },
130
- context: { type: 'object' },
131
- },
132
- },
133
- annotations: {
134
- title: 'Move Layer Into Group',
135
- idempotentHint: false,
136
- },
137
- },
138
- handler: async (args) => moveLayerToGroup(connection, snippetClient, args),
139
- },
140
- {
141
- tool: {
142
- name: 'ps_set_group_blend_mode',
143
- description: "Set a group's blend mode. The default for new groups is PASSTHROUGH (adjustments inside affect layers below the group). Change to NORMAL when treating the group as a single composite — required for masking a stack of adjustments with one mask.",
144
- inputSchema: setGroupBlendModeSchema,
145
- outputSchema: {
146
- type: 'object',
147
- properties: {
148
- set: { type: 'boolean' },
149
- groupName: { type: 'string' },
150
- blendMode: { type: 'string' },
151
- context: { type: 'object' },
152
- },
153
- },
154
- annotations: {
155
- title: 'Set Group Blend Mode',
156
- idempotentHint: true,
157
- },
158
- },
159
- handler: async (args) => setGroupBlendMode(connection, snippetClient, args),
160
- },
161
- {
162
- tool: {
163
- name: 'ps_ungroup',
164
- description: 'DESTRUCTIVE structural change: Dissolve a group, leaving its contents at the parent level in their existing stack order. The group itself is removed. Requires confirm:true. Recoverable only via Edit > Undo.',
165
- inputSchema: ungroupSchema,
166
- outputSchema: {
167
- type: 'object',
168
- properties: {
169
- ungrouped: { type: 'boolean' },
170
- groupName: { type: 'string' },
171
- children_promoted: { type: 'number' },
172
- child_names: { type: 'array', items: { type: 'string' } },
173
- context: { type: 'object' },
174
- },
175
- },
176
- annotations: {
177
- title: 'Ungroup Layer Set (destructive structural change)',
183
+ title: 'Group',
178
184
  destructiveHint: true,
179
185
  idempotentHint: false,
180
186
  },
181
187
  },
182
- handler: async (args) => ungroup(connection, snippetClient, args),
188
+ handler: async (args) => groupDispatch(connection, snippetClient, args),
183
189
  },
184
190
  {
185
191
  tool: {
186
- name: 'ps_create_clipping_mask',
187
- description: 'Clip the active layer to the layer directly below it (uses that layer as the alpha source). Non-destructive — the upper layer is unchanged; PS just paints it only where the layer below has pixels. Common for masking adjustment-layer effect to a single underlying layer, or constraining a texture/photo to a shape. Equivalent to the PS menu Layer > Create Clipping Mask (Ctrl+Alt+G). Reverse it with the PS menu Layer > Release Clipping Mask, or ps_undo. This is the standalone primitive — the add_adjustment_layer tool already accepts clip_to_below for the adjustment-layer-specific case.',
188
- inputSchema: { type: 'object', properties: {}, required: [] },
192
+ name: 'ps_clipping_mask',
193
+ description: 'Clip or un-clip the active layer against the layer directly below it — choose with `op`. `create`: use the layer below as the alpha source; PS paints the active layer only where the layer below has pixels. Non-destructive — the upper layer is unchanged. Common for constraining a texture/photo to a shape, or masking an effect to a single underlying layer (the add_adjustment_layer tool already accepts clip_to_below for the adjustment-layer-specific case). Equivalent to Layer > Create Clipping Mask (Ctrl+Alt+G). `release`: the inverse — the layer returns to compositing against the whole canvas. Both ops are idempotent: create no-ops (already_clipped:true) on an already-clipped layer; release no-ops (released:false) on a non-clipped layer.',
194
+ inputSchema: clippingMaskSchema,
189
195
  outputSchema: {
190
196
  type: 'object',
191
197
  properties: {
192
198
  clipped: { type: 'boolean' },
193
- layerName: { type: 'string' },
194
- context: { type: 'object' },
195
- },
196
- },
197
- annotations: {
198
- title: 'Create Clipping Mask',
199
- idempotentHint: false,
200
- },
201
- },
202
- handler: async (args) => createClippingMask(connection, snippetClient, args),
203
- },
204
- {
205
- tool: {
206
- name: 'ps_release_clipping_mask',
207
- description: "Release the active layer's clipping mask (the inverse of ps_create_clipping_mask). The layer returns to compositing against the whole canvas instead of just the layer below. Equivalent to PS menu Layer > Release Clipping Mask. Idempotent on a non-clipped layer (PS silently no-ops).",
208
- inputSchema: { type: 'object', properties: {}, required: [] },
209
- outputSchema: {
210
- type: 'object',
211
- properties: {
199
+ already_clipped: { type: 'boolean' },
212
200
  released: { type: 'boolean' },
213
201
  layerName: { type: 'string' },
214
202
  context: { type: 'object' },
215
203
  },
216
204
  },
217
205
  annotations: {
218
- title: 'Release Clipping Mask',
206
+ title: 'Clipping Mask',
219
207
  idempotentHint: true,
220
208
  },
221
209
  },
222
- handler: async (args) => releaseClippingMask(connection, snippetClient, args),
223
- },
224
- {
225
- tool: {
226
- name: 'ps_delete_group',
227
- description: 'DESTRUCTIVE: Delete a group AND ALL its contents (including nested groups and their layers). Requires confirm:true because the destructive scope is much larger than a single-layer delete. Recoverable only via Edit > Undo. To dissolve a group while keeping its contents, use ps_ungroup instead.',
228
- inputSchema: deleteGroupSchema,
229
- outputSchema: {
230
- type: 'object',
231
- properties: {
232
- deleted: { type: 'boolean' },
233
- groupName: { type: 'string' },
234
- descendants_deleted: { type: 'number' },
235
- context: { type: 'object' },
236
- },
237
- },
238
- annotations: {
239
- title: 'Delete Group (destructive, recursive)',
240
- destructiveHint: true,
241
- idempotentHint: false,
242
- },
243
- },
244
- handler: async (args) => deleteGroup(connection, snippetClient, args),
210
+ handler: async (args) => clippingMask(connection, snippetClient, args),
245
211
  },
246
212
  ];
247
213
  }
248
- async function createClippingMask(connection, snippetClient, rawArgs) {
249
- return runSnippetTool({
250
- connection,
251
- snippetClient,
252
- rawArgs,
253
- schema: emptySchema,
254
- snippet: 'createClippingMask',
255
- errorPrefix: 'Error creating clipping mask',
256
- successText: (result) => `Clipped layer "${result.layerName ?? ''}" to the layer below.`,
257
- });
214
+ async function clippingMask(connection, snippetClient, rawArgs) {
215
+ const op = rawArgs.op;
216
+ const { op: _omit, ...rest } = rawArgs;
217
+ switch (op) {
218
+ case 'create':
219
+ return runSnippetTool({
220
+ connection,
221
+ snippetClient,
222
+ rawArgs: rest,
223
+ schema: clippingMaskOpArgsSchema,
224
+ snippet: 'createClippingMask',
225
+ errorPrefix: 'Error creating clipping mask',
226
+ successText: (result) => {
227
+ const r = result;
228
+ return r.already_clipped === true
229
+ ? `Layer "${r.layerName ?? ''}" is already clipped — nothing to do.`
230
+ : `Clipped layer "${r.layerName ?? ''}" to the layer below.`;
231
+ },
232
+ });
233
+ case 'release':
234
+ return runSnippetTool({
235
+ connection,
236
+ snippetClient,
237
+ rawArgs: rest,
238
+ schema: clippingMaskOpArgsSchema,
239
+ snippet: 'releaseClippingMask',
240
+ errorPrefix: 'Error releasing clipping mask',
241
+ successText: (result) => {
242
+ const r = result;
243
+ return r.released === false
244
+ ? `Layer "${r.layerName ?? ''}" is not clipped — nothing to release.`
245
+ : `Released clipping mask on layer "${r.layerName ?? ''}".`;
246
+ },
247
+ });
248
+ default:
249
+ return unknownDiscriminator('clipping_mask op', op, CLIPPING_MASK_OPS);
250
+ }
258
251
  }
259
- async function releaseClippingMask(connection, snippetClient, rawArgs) {
260
- return runSnippetTool({
261
- connection,
262
- snippetClient,
263
- rawArgs,
264
- schema: emptySchema,
265
- snippet: 'releaseClippingMask',
266
- errorPrefix: 'Error releasing clipping mask',
267
- successText: (result) => `Released clipping mask on layer "${result.layerName ?? ''}".`,
268
- });
252
+ async function groupDispatch(connection, snippetClient, rawArgs) {
253
+ const op = rawArgs.op;
254
+ const { op: _omit, ...rest } = rawArgs;
255
+ switch (op) {
256
+ case 'create':
257
+ return createGroup(connection, snippetClient, rest);
258
+ case 'delete':
259
+ return deleteGroup(connection, snippetClient, rest);
260
+ case 'ungroup':
261
+ return ungroup(connection, snippetClient, rest);
262
+ case 'add_layer':
263
+ return moveLayerToGroup(connection, snippetClient, rest);
264
+ case 'set_blend_mode':
265
+ return setGroupBlendMode(connection, snippetClient, rest);
266
+ default:
267
+ return unknownDiscriminator('group op', op, GROUP_OPS);
268
+ }
269
269
  }
270
270
  async function createGroup(connection, snippetClient, rawArgs) {
271
271
  return runSnippetTool({
@@ -39,7 +39,7 @@ export function createHistoryTools(connection, snippetClient) {
39
39
  {
40
40
  tool: {
41
41
  name: 'ps_undo',
42
- description: 'Step backward in the document history (equivalent to Ctrl/Cmd+Z). WHEN TO REACH FOR THIS: after an experimental destructive bake (ps_merge mode=visible/flatten, ps_apply_filter type=*_blur/sharpen/noise) produced a wrong result; after a play_action whose scope you mispredicted; or to revert an entire branch of exploration. Non-destructive workflows (adjustment layers + masks) rarely need this — just tweak/delete the offending layer instead. Reversible via ps_redo as long as no new edit has been made since. Returns current history state, remaining steps, and document context.',
42
+ description: 'Step backward in the document history (equivalent to Ctrl/Cmd+Z). WHEN TO REACH FOR THIS: after an experimental destructive bake (ps_merge mode=visible/flatten, ps_filter op=apply type=*_blur/sharpen/noise) produced a wrong result; after a play_action whose scope you mispredicted; or to revert an entire branch of exploration. Non-destructive workflows (adjustment layers + masks) rarely need this — just tweak/delete the offending layer instead. Reversible via ps_redo as long as no new edit has been made since. Returns current history state, remaining steps, and document context.',
43
43
  inputSchema: undoSchema,
44
44
  outputSchema: HISTORY_STEP_RESULT_SCHEMA,
45
45
  annotations: {
@@ -2,7 +2,14 @@ import { getMetadata, getMetadataSchema } from './metadata-tools.js';
2
2
  import { getHistory } from './history-tools.js';
3
3
  import { getLayerTree } from './layer-tools.js';
4
4
  import { getSelectionInfoHandler } from './selection-tools.js';
5
- const INSPECT_WHATS = ['metadata', 'layer_tree', 'history', 'selection_info'];
5
+ import { getSmartObjectInfoHandler } from './smart-object-tools.js';
6
+ const INSPECT_WHATS = [
7
+ 'metadata',
8
+ 'layer_tree',
9
+ 'history',
10
+ 'selection_info',
11
+ 'smart_object',
12
+ ];
6
13
  const INSPECT_INPUT_SCHEMA = {
7
14
  type: 'object',
8
15
  properties: {
@@ -13,7 +20,8 @@ const INSPECT_INPUT_SCHEMA = {
13
20
  'metadata: document/IPTC/camera-EXIF/GPS/ACR develop settings + active context (optionally subset with `sections`; sections=["context"] is the cheap orientation probe). ' +
14
21
  'layer_tree: the full recursive layer tree (name/kind/visibility/opacity/blend/clipping/bounds) — use whenever you need what is inside a group. ' +
15
22
  'history: all history states + the current cursor, for deciding how far to undo. ' +
16
- 'selection_info: current selection bounds/coverage/edge-complexity without modifying anything.',
23
+ 'selection_info: current selection bounds/coverage/edge-complexity without modifying anything. ' +
24
+ 'smart_object: whether the ACTIVE layer is a Smart Object and, if so, whether its source is embedded or linked to a file on disk, plus how many Smart Filters it carries.',
17
25
  },
18
26
  ...getMetadataSchema.properties,
19
27
  },
@@ -24,7 +32,7 @@ export function createInspectTools(connection, snippetClient) {
24
32
  {
25
33
  tool: {
26
34
  name: 'ps_inspect',
27
- description: 'Read-only document inspection — choose with `what` (metadata / layer_tree / history / selection_info). This is the assess/orientation surface: call it at the start of a workflow and whenever you need fresh state. For metadata, pass `sections` to subset (e.g. ["context"] for a cheap probe). For an IMAGE-based check use ps_get_preview; for NUMERIC verification use ps_get_histogram / ps_compare_regions / ps_get_layer_bounds_diff (these stay separate, named tools on purpose). Read-only and idempotent.',
35
+ description: 'Read-only document inspection — choose with `what` (metadata / layer_tree / history / selection_info / smart_object). This is the assess/orientation surface: call it at the start of a workflow and whenever you need fresh state. For metadata, pass `sections` to subset (e.g. ["context"] for a cheap probe). For an IMAGE-based check use ps_get_preview; for NUMERIC verification use ps_get_histogram / ps_compare_regions / ps_get_layer_bounds_diff (these stay separate, named tools on purpose). Read-only and idempotent.',
28
36
  inputSchema: INSPECT_INPUT_SCHEMA,
29
37
  outputSchema: {
30
38
  type: 'object',
@@ -47,6 +55,15 @@ export function createInspectTools(connection, snippetClient) {
47
55
  canRedo: { type: 'boolean' },
48
56
  states: { type: 'array' },
49
57
  selection_info: { type: 'object' },
58
+ is_smart_object: { type: 'boolean' },
59
+ linked: { type: 'boolean' },
60
+ file_reference: { type: ['string', 'null'] },
61
+ document_id: { type: ['string', 'null'] },
62
+ placed: { type: ['string', 'null'] },
63
+ smart_filter_count: { type: 'number' },
64
+ layer_name: { type: 'string' },
65
+ layer_kind: { type: 'string' },
66
+ bounds: { type: 'array' },
50
67
  },
51
68
  },
52
69
  annotations: {
@@ -72,6 +89,8 @@ async function inspect(connection, snippetClient, rawArgs) {
72
89
  return getHistory(connection, snippetClient);
73
90
  case 'selection_info':
74
91
  return getSelectionInfoHandler(connection, snippetClient);
92
+ case 'smart_object':
93
+ return getSmartObjectInfoHandler(connection, snippetClient);
75
94
  default:
76
95
  return {
77
96
  content: [
@@ -19,7 +19,7 @@ const deleteLayerSchema = {
19
19
  },
20
20
  };
21
21
  const PS_TEXT_COORD_MAX = 300_000;
22
- const createTextLayerSchema = {
22
+ export const createTextLayerSchema = {
23
23
  type: 'object',
24
24
  properties: {
25
25
  text: {
@@ -195,7 +195,7 @@ export function createLayerTools(connection, snippetClient) {
195
195
  {
196
196
  tool: {
197
197
  name: 'ps_create_layer',
198
- description: 'Create a new empty raster layer above the currently active layer. Non-destructive. Use ps_create_text_layer for text, ps_add_adjustment_layer for adjustments.',
198
+ description: 'Create a new empty raster layer above the currently active layer. Non-destructive. Use ps_text (op=create) for text, ps_add_adjustment_layer for adjustments.',
199
199
  inputSchema: createLayerSchema,
200
200
  outputSchema: {
201
201
  type: 'object',
@@ -220,7 +220,7 @@ export function createLayerTools(connection, snippetClient) {
220
220
  {
221
221
  tool: {
222
222
  name: 'ps_delete_layer',
223
- description: 'DESTRUCTIVE: Delete a layer. With no arg, deletes the currently active layer (backward-compatible). With `name`, recurses into groups and deletes the first layer matching that name — useful for cleanup workflows where the dead layer is not currently active. Recoverable only via Edit > Undo. To delete a group and all its contents, use ps_delete_group.',
223
+ description: 'DESTRUCTIVE: Delete a layer. With no arg, deletes the currently active layer (backward-compatible). With `name`, recurses into groups and deletes the first LAYER matching that name — useful for cleanup workflows where the dead layer is not currently active. A name that matches a group is refused rather than deleted; use ps_group(op=delete) to delete a group and all its contents. Recoverable only via Edit > Undo.',
224
224
  inputSchema: deleteLayerSchema,
225
225
  outputSchema: {
226
226
  type: 'object',
@@ -238,29 +238,6 @@ export function createLayerTools(connection, snippetClient) {
238
238
  },
239
239
  handler: async (args) => deleteLayer(connection, snippetClient, args),
240
240
  },
241
- {
242
- tool: {
243
- name: 'ps_create_text_layer',
244
- description: 'Create a new text layer with the given content, position, and font size. The text is editable (vector). Use ps_set_text (property=font / color) afterwards to style.',
245
- inputSchema: createTextLayerSchema,
246
- outputSchema: {
247
- type: 'object',
248
- properties: {
249
- created: { type: 'boolean' },
250
- layerName: { type: 'string' },
251
- text: { type: 'string' },
252
- position: { type: 'object' },
253
- fontSize: { type: 'number' },
254
- context: { type: 'object' },
255
- },
256
- },
257
- annotations: {
258
- title: 'Create Text Layer',
259
- idempotentHint: false,
260
- },
261
- },
262
- handler: async (args) => createTextLayer(connection, snippetClient, args),
263
- },
264
241
  {
265
242
  tool: {
266
243
  name: 'ps_fill_layer',
@@ -382,7 +359,7 @@ async function deleteLayer(connection, snippetClient, rawArgs) {
382
359
  },
383
360
  });
384
361
  }
385
- async function createTextLayer(connection, snippetClient, rawArgs) {
362
+ export async function createTextLayer(connection, snippetClient, rawArgs) {
386
363
  return runSnippetTool({
387
364
  connection,
388
365
  snippetClient,
@@ -19,6 +19,8 @@ Every non-trivial edit follows the same six phases:
19
19
 
20
20
  1. **Assess** — \`ps_inspect\` (what=metadata / layer_tree / history /
21
21
  selection_info) + \`ps_get_preview\` to understand what you're working with.
22
+ If \`ps_open_document\` reported \`is_raw_source: true\`, see
23
+ "Raw-sourced documents" below.
22
24
  2. **Plan** — name the intent, the layers/regions involved, and the
23
25
  exit criteria. Tell the user before you execute.
24
26
  3. **Enact** — call the tools.
@@ -31,6 +33,33 @@ Every non-trivial edit follows the same six phases:
31
33
  \`ps_save_psd\`. Confirm the user wants to flatten before
32
34
  destructive saves.
33
35
 
36
+ ## Raw-sourced documents
37
+
38
+ \`ps_open_document\` reports \`is_raw_source: true\` for raw captures
39
+ (DNG, NEF, CR3, ARW, …) — the open applied last-used/default Camera
40
+ Raw settings, so no deliberate develop has happened yet. When
41
+ \`tools/list\` includes a camera-raw develop tool, the FIRST enacting
42
+ step on a raw document is that develop pass on the base smart object —
43
+ NOT stacked Levels/Curves adjustment layers. The develop pass owns
44
+ global tone and color; adjustment layers come after, for local/masked
45
+ corrections and finishing moves.
46
+
47
+ Make the first develop pass a complete grade in ONE call, not a timid
48
+ nudge: tone endpoints (exposure, contrast, highlights/shadows AND
49
+ whites/blacks), presence (texture, clarity, dehaze), a parametric
50
+ curve, white balance + vibrance + per-channel HSL where colors need
51
+ steering, color grading, capture sharpening + noise reduction (raws
52
+ get none by default), optics/vignette where they serve the image.
53
+ Three sliders at small values reads as "untouched." Large moves are
54
+ safe — the develop is a re-editable smart filter, nothing bakes:
55
+ start bold, check preview + histogram, ease off.
56
+
57
+ To refine, call the develop tool again in its adjust-existing mode —
58
+ it preserves every slider you don't pass. Never add a second
59
+ camera-raw filter. If no camera-raw develop tool is in \`tools/list\`,
60
+ say so in one sentence and build global tone with adjustment layers.
61
+ Explicit user instructions always win.
62
+
34
63
  ## Capabilities map
35
64
 
36
65
  This map covers the workflow categories. For specific tool names and
@@ -40,8 +69,9 @@ schemas in this session, consult \`tools/list\`.
40
69
  covers brightness/contrast, levels, curves, hue/sat, color balance,
41
70
  photo filter, vibrance, channel mixer, selective color, black & white,
42
71
  gradient map, exposure, color lookup, invert. Always prefer this over
43
- destructive filters for tone/color work.
44
- - **Filters (destructive)** — \`ps_apply_filter\` with a
72
+ destructive filters for tone/color work — except raw-sourced
73
+ documents: the camera-raw develop pass goes first (see above).
74
+ - **Filters (destructive)** — \`ps_filter\` with a
45
75
  \`type\` (gaussian_blur, motion_blur, lens_blur, radial_blur, sharpen,
46
76
  smart_sharpen, noise, reduce_noise, high_pass, pixelate, distort,
47
77
  displace, oil_paint). Auto-duplicates the active layer by default so
@@ -81,16 +111,15 @@ schemas in this session, consult \`tools/list\`.
81
111
  locally. Reach for these to get REAL coordinates rather than
82
112
  estimating from the preview — they're the antidote to spatial
83
113
  guessing.
84
- - **Text** — \`ps_create_text_layer\`, then \`ps_set_text\`
85
- (property font / color / alignment / content; font resolves family
86
- names to PostScript).
114
+ - **Text** — \`ps_text\` (op create, then set_font / set_color /
115
+ set_alignment / set_content; font resolves family names to
116
+ PostScript).
87
117
  - **Image** — \`ps_place_image\` (links external files as smart
88
118
  objects), \`ps_resize_image\`, \`ps_crop_document\`,
89
119
  \`ps_convert_image_mode\`, open / close / save_psd, and
90
120
  \`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\`.
121
+ - **Groups** — \`ps_group\` (op create / delete / ungroup /
122
+ add_layer / set_blend_mode).
94
123
  - **Verification** — see below; this is the most underused part of
95
124
  the surface.
96
125
  - **Templates** — when template tools are present in \`tools/list\`,
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { getContextInfo, normNameHelper } from '../api/extendscript/_helpers.js';
2
+ import { getContextInfo, normNameHelper, notFoundMessageHelper, } from '../api/extendscript/_helpers.js';
3
3
  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';
@@ -899,6 +899,7 @@ async function getLayerBoundsDiff(connection, rawArgs) {
899
899
  var doc = app.activeDocument;
900
900
 
901
901
  ${normNameHelper}
902
+ ${notFoundMessageHelper}
902
903
  // Em-dash / en-dash tolerant comparison (Bug I) via normName — the
903
904
  // LLM routinely swaps these silently; raw equality would miss.
904
905
  // Depth cap is defense-in-depth (see selectLayer's identical comment).
@@ -919,7 +920,7 @@ async function getLayerBoundsDiff(connection, rawArgs) {
919
920
  return null;
920
921
  }
921
922
  var lyr = findLayerByName(doc.layers);
922
- if (!lyr) throw new Error('Layer not found: ' + ${jsLit(layer)});
923
+ if (!lyr) throw new Error(__notFoundMessage('Layer', ${jsLit(layer)}, false));
923
924
 
924
925
  var b = (lyr.boundsNoEffects !== undefined) ? lyr.boundsNoEffects : lyr.bounds;
925
926
  var aLeft = b[0].as('px');
@@ -367,7 +367,7 @@ export function createSceneTools(connection, snippetClient, opts = {}) {
367
367
  {
368
368
  tool: {
369
369
  name: 'ps_read_scene',
370
- 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.',
370
+ description: 'The full scene model — run this before a spatially-targeted edit, not the cheaper ps_detect: 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), composition geometry (which thirds cell the subject sits in, balance, headroom), plus an annotated preview and the menu of selectable named regions. Built using LOCAL on-device vision + classical CV; the image never leaves the machine. Select regions by name with ps_select_by_reference instead of guessing a rectangle. Read-only: renders a throwaway duplicate. Perception is cached per document state, so repeated reads are cheap.',
371
371
  inputSchema: sceneSchema,
372
372
  outputSchema: {
373
373
  type: 'object',