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.
- package/README.md +9 -9
- package/dist/api/extendscript/_helpers.js +70 -2
- package/dist/api/photoshop-api.js +17 -2
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/activate.js +1 -1
- package/dist/cli/config.js +1 -1
- package/dist/cli/deactivate.js +1 -1
- package/dist/core/raw-develop-state.js +13 -0
- package/dist/core/server.js +147 -18
- package/dist/core/tool-groups.js +18 -21
- package/dist/core/tool-tiers.js +7 -10
- package/dist/detection/runtime.js +2 -2
- package/dist/modules/ce/index.js +2 -0
- package/dist/perception/region-scorer.js +1 -1
- package/dist/platform/connection.js +15 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/client.js +15 -2
- package/dist/telemetry/sanitize.js +1 -0
- package/dist/tools/adjustment-tools.js +21 -3
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/document-tools.js +5 -2
- package/dist/tools/filter-tools.js +118 -15
- package/dist/tools/group-tools.js +146 -146
- package/dist/tools/history-tools.js +1 -1
- package/dist/tools/inspect-tools.js +22 -3
- package/dist/tools/layer-tools.js +4 -27
- package/dist/tools/overview-tools.js +37 -8
- package/dist/tools/preview-tools.js +3 -2
- package/dist/tools/scene-tools.js +1 -1
- package/dist/tools/selection-tools.js +91 -18
- package/dist/tools/sky-tools.js +126 -0
- package/dist/tools/smart-object-tools.js +163 -0
- package/dist/tools/text-tools.js +62 -41
- package/dist/update/check.js +65 -6
- package/dist/update/session-fixes.js +33 -0
- package/dist/utils/jsx.js +2 -1
- package/dist/utils/operation-timeouts.js +2 -0
- package/dist/utils/session-log.js +46 -6
- package/dist/utils/temp.js +1 -1
- package/dist/utils/tool-helpers.js +23 -0
- package/dist/version.js +1 -1
- 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: '
|
|
91
|
-
description:
|
|
92
|
-
inputSchema:
|
|
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
|
-
|
|
99
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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) =>
|
|
188
|
+
handler: async (args) => groupDispatch(connection, snippetClient, args),
|
|
183
189
|
},
|
|
184
190
|
{
|
|
185
191
|
tool: {
|
|
186
|
-
name: '
|
|
187
|
-
description: 'Clip the active layer
|
|
188
|
-
inputSchema:
|
|
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
|
-
|
|
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: '
|
|
206
|
+
title: 'Clipping Mask',
|
|
219
207
|
idempotentHint: true,
|
|
220
208
|
},
|
|
221
209
|
},
|
|
222
|
-
handler: async (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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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** — \`
|
|
85
|
-
|
|
86
|
-
|
|
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** — \`
|
|
92
|
-
|
|
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
|
|
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: '
|
|
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',
|