editmamei 0.22.3 → 0.23.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/dist/api/extendscript/_helpers.js +185 -1
- 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/core/server.js +32 -0
- package/dist/license/entitlement.js +2 -2
- package/dist/license/ping-refresh.js +8 -4
- package/dist/platform/macos-executor.js +12 -6
- package/dist/platform/run-child.js +6 -3
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +17 -1
- package/dist/tools/document-tools.js +35 -3
- package/dist/tools/group-tools.js +17 -2
- package/dist/tools/layer-properties-tools.js +65 -16
- package/dist/tools/layer-tools.js +27 -2
- package/dist/tools/preview-tools.js +2 -1
- package/dist/tools/selection-tools.js +3 -2
- package/dist/tools/shape-tools.js +16 -1
- package/dist/utils/operation-timeouts.js +6 -0
- package/dist/utils/session-log.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -205,7 +205,29 @@ export function duplicateForOp(opName, applyToActiveLayer) {
|
|
|
205
205
|
var __opTargetIsCopy = true;
|
|
206
206
|
`;
|
|
207
207
|
}
|
|
208
|
+
export const countLayersRecursiveHelper = `
|
|
209
|
+
function __countLayersRecursive(layers) {
|
|
210
|
+
var total = 0;
|
|
211
|
+
var n = 0;
|
|
212
|
+
try { n = layers.length; } catch (eN) { n = 0; }
|
|
213
|
+
for (var i = 0; i < n; i++) {
|
|
214
|
+
var candidate = null;
|
|
215
|
+
try { candidate = layers[i]; } catch (eC) { continue; }
|
|
216
|
+
if (!candidate) continue;
|
|
217
|
+
total++;
|
|
218
|
+
var isGroup = false;
|
|
219
|
+
try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
|
|
220
|
+
if (isGroup) {
|
|
221
|
+
var childLayers = null;
|
|
222
|
+
try { childLayers = candidate.layers; } catch (eCl) { childLayers = null; }
|
|
223
|
+
if (childLayers) total += __countLayersRecursive(childLayers);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return total;
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
208
229
|
export const getContextInfo = `
|
|
230
|
+
${countLayersRecursiveHelper}
|
|
209
231
|
function getContextInfo() {
|
|
210
232
|
// The whole body is wrapped in try/catch — a destructive op that
|
|
211
233
|
// succeeded but then triggered "document closed" / "active layer gone"
|
|
@@ -228,6 +250,7 @@ function getContextInfo() {
|
|
|
228
250
|
resolution: doc.resolution,
|
|
229
251
|
colorMode: String(doc.mode),
|
|
230
252
|
layerCount: doc.layers.length,
|
|
253
|
+
total_layer_count: __countLayersRecursive(doc.layers),
|
|
231
254
|
hasSelection: (function () {
|
|
232
255
|
// ExtendScript throws "No such element" when accessing doc.selection.bounds
|
|
233
256
|
// with no active selection, and that error is NOT catchable when DoJavaScript
|
|
@@ -242,12 +265,35 @@ function getContextInfo() {
|
|
|
242
265
|
|
|
243
266
|
if (doc.activeLayer) {
|
|
244
267
|
var layer = doc.activeLayer;
|
|
268
|
+
|
|
269
|
+
// layer.visible on the ExtendScript DOM returns EFFECTIVE
|
|
270
|
+
// visibility (parent-chain AND own flag) NO MATTER which proxy
|
|
271
|
+
// resolves it -- re-reading through a fresh doc.layers walk does
|
|
272
|
+
// NOT recover the layer's own flag (verified live 2026-07: a
|
|
273
|
+
// visible child inside a hidden group reads visible:false from
|
|
274
|
+
// every DOM proxy, active-layer or walked). The own flag is only
|
|
275
|
+
// readable via Action Manager's Vsbl property, keyed by the
|
|
276
|
+
// layer's stable id (putIdentifier, not putName -- names aren't
|
|
277
|
+
// unique). Confirmed live on both an ArtLayer and a LayerSet.
|
|
278
|
+
// Falls back to the DOM (effective) value if the AM read throws,
|
|
279
|
+
// so a failure degrades to the old behavior instead of breaking
|
|
280
|
+
// the context block every tool returns.
|
|
281
|
+
var __effectiveVisible = layer.visible;
|
|
282
|
+
var __ownVisible = __effectiveVisible;
|
|
283
|
+
try {
|
|
284
|
+
var __visRef = new ActionReference();
|
|
285
|
+
__visRef.putProperty(app.charIDToTypeID('Prpr'), app.charIDToTypeID('Vsbl'));
|
|
286
|
+
__visRef.putIdentifier(app.charIDToTypeID('Lyr '), layer.id);
|
|
287
|
+
__ownVisible = app.executeActionGet(__visRef).getBoolean(app.charIDToTypeID('Vsbl'));
|
|
288
|
+
} catch (eOwn) {}
|
|
289
|
+
|
|
245
290
|
context.activeLayer = {
|
|
246
291
|
name: layer.name,
|
|
247
292
|
kind: String(layer.kind),
|
|
248
293
|
opacity: layer.opacity,
|
|
249
294
|
blendMode: String(layer.blendMode),
|
|
250
|
-
visible:
|
|
295
|
+
visible: __ownVisible,
|
|
296
|
+
effectively_visible: __effectiveVisible,
|
|
251
297
|
locked: layer.allLocked,
|
|
252
298
|
isBackground: layer.isBackgroundLayer
|
|
253
299
|
};
|
|
@@ -302,3 +348,141 @@ function getMinimalContextInfo() {
|
|
|
302
348
|
}
|
|
303
349
|
}
|
|
304
350
|
`;
|
|
351
|
+
export const parentPathHelper = `
|
|
352
|
+
function __parentPathOf(doc, layer) {
|
|
353
|
+
function __ppWalk(layers, trail) {
|
|
354
|
+
var n = 0;
|
|
355
|
+
try { n = layers.length; } catch (eN) {}
|
|
356
|
+
for (var i = 0; i < n; i++) {
|
|
357
|
+
var candidate = null;
|
|
358
|
+
try { candidate = layers[i]; } catch (eC) { continue; }
|
|
359
|
+
if (!candidate) continue;
|
|
360
|
+
if (candidate === layer) return trail;
|
|
361
|
+
var isGroup = false;
|
|
362
|
+
try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
|
|
363
|
+
if (isGroup) {
|
|
364
|
+
var cname = null;
|
|
365
|
+
try { cname = candidate.name; } catch (eNm) {}
|
|
366
|
+
var childLayers = null;
|
|
367
|
+
try { childLayers = candidate.layers; } catch (eCl) {}
|
|
368
|
+
if (childLayers) {
|
|
369
|
+
var found = __ppWalk(childLayers, trail.concat([cname]));
|
|
370
|
+
if (found !== null) return found;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
return __ppWalk(doc.layers, []);
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
export const hoistFromActiveGroupHelper = `
|
|
380
|
+
function __hoistFromActiveGroupIfNeeded(doc, preMkActive, newLayer, intoActiveGroup) {
|
|
381
|
+
if (intoActiveGroup) return false;
|
|
382
|
+
var preWasGroup = false;
|
|
383
|
+
try { preWasGroup = (preMkActive instanceof LayerSet); } catch (eG) {}
|
|
384
|
+
if (!preWasGroup) return false;
|
|
385
|
+
var landedInside = false;
|
|
386
|
+
try {
|
|
387
|
+
var n = preMkActive.layers.length;
|
|
388
|
+
for (var i = 0; i < n; i++) {
|
|
389
|
+
if (preMkActive.layers[i] === newLayer) { landedInside = true; break; }
|
|
390
|
+
}
|
|
391
|
+
} catch (eIn) {}
|
|
392
|
+
if (!landedInside) return false;
|
|
393
|
+
try {
|
|
394
|
+
newLayer.move(preMkActive, ElementPlacement.PLACEBEFORE);
|
|
395
|
+
try { doc.activeLayer = newLayer; } catch (eA) {}
|
|
396
|
+
return true;
|
|
397
|
+
} catch (eMove) {}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
`;
|
|
401
|
+
export const layerResolveHelpers = `
|
|
402
|
+
function __safeGet(fn, fallback) {
|
|
403
|
+
try { var v = fn(); return (v === undefined ? fallback : v); }
|
|
404
|
+
catch (e) { return fallback; }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function __findLayerPath(layers, target, prefix) {
|
|
408
|
+
var n = __safeGet(function () { return layers.length; }, 0);
|
|
409
|
+
for (var i = 0; i < n; i++) {
|
|
410
|
+
var candidate = __safeGet(function () { return layers[i]; }, null);
|
|
411
|
+
if (!candidate) continue;
|
|
412
|
+
if (candidate === target) return prefix.concat([i]);
|
|
413
|
+
var isGroup = false;
|
|
414
|
+
try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
|
|
415
|
+
if (isGroup) {
|
|
416
|
+
var childLayers = __safeGet(function () { return candidate.layers; }, null);
|
|
417
|
+
if (childLayers) {
|
|
418
|
+
var found = __findLayerPath(childLayers, target, prefix.concat([i]));
|
|
419
|
+
if (found) return found;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function __findLayerById(layers, id) {
|
|
427
|
+
var n = __safeGet(function () { return layers.length; }, 0);
|
|
428
|
+
for (var i = 0; i < n; i++) {
|
|
429
|
+
var candidate = __safeGet(function () { return layers[i]; }, null);
|
|
430
|
+
if (!candidate) continue;
|
|
431
|
+
var cid = __safeGet(function () { return candidate.id; }, null);
|
|
432
|
+
if (cid === id) return candidate;
|
|
433
|
+
var isGroup = false;
|
|
434
|
+
try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
|
|
435
|
+
if (isGroup) {
|
|
436
|
+
var childLayers = __safeGet(function () { return candidate.layers; }, null);
|
|
437
|
+
if (childLayers) {
|
|
438
|
+
var found = __findLayerById(childLayers, id);
|
|
439
|
+
if (found) return found;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function __resolvePath(doc, path) {
|
|
447
|
+
var current = doc;
|
|
448
|
+
for (var i = 0; i < path.length; i++) {
|
|
449
|
+
var idx = path[i];
|
|
450
|
+
current = __safeGet(function () { return current.layers[idx]; }, null);
|
|
451
|
+
if (!current) return null;
|
|
452
|
+
}
|
|
453
|
+
return current;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function __captureLayerIdentity(doc, layer) {
|
|
457
|
+
var id = null;
|
|
458
|
+
try { if (typeof layer.id !== 'undefined') { id = layer.id; } } catch (eId) {}
|
|
459
|
+
var path = null;
|
|
460
|
+
if (id === null) {
|
|
461
|
+
path = __findLayerPath(doc.layers, layer, []);
|
|
462
|
+
}
|
|
463
|
+
return { id: id, path: path };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function __resolveLayerFresh(doc, identity) {
|
|
467
|
+
if (identity.id !== null) {
|
|
468
|
+
var byId = __findLayerById(doc.layers, identity.id);
|
|
469
|
+
if (byId) return byId;
|
|
470
|
+
}
|
|
471
|
+
if (identity.path) {
|
|
472
|
+
return __resolvePath(doc, identity.path);
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function __resolveLayerFreshOrActive(doc, identity) {
|
|
478
|
+
var resolved = __resolveLayerFresh(doc, identity);
|
|
479
|
+
if (resolved) return resolved;
|
|
480
|
+
// Identity resolution came up empty even though the write may have
|
|
481
|
+
// landed — e.g. Photoshop auto-promoting a Background layer as a side
|
|
482
|
+
// effect of the property write. Fall back to the current active layer
|
|
483
|
+
// (these setters always operate on doc.activeLayer, and Photoshop keeps
|
|
484
|
+
// it pointed at the promoted layer) rather than treating an unresolved
|
|
485
|
+
// identity as proof the write failed.
|
|
486
|
+
return __safeGet(function () { return doc.activeLayer; }, null);
|
|
487
|
+
}
|
|
488
|
+
`;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/core/server.js
CHANGED
|
@@ -330,6 +330,37 @@ export class EditmameiServer {
|
|
|
330
330
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
|
+
async ensureEntitledModuleFresh(delivery = {}) {
|
|
334
|
+
if (this.moduleSkipReason !== null)
|
|
335
|
+
return;
|
|
336
|
+
if (!isProEntitled())
|
|
337
|
+
return;
|
|
338
|
+
const underTest = process.env.VITEST !== undefined || process.env.NODE_ENV === 'test';
|
|
339
|
+
if (underTest && !delivery.fetchImpl)
|
|
340
|
+
return;
|
|
341
|
+
const license = readLicense();
|
|
342
|
+
if (!license)
|
|
343
|
+
return;
|
|
344
|
+
try {
|
|
345
|
+
const prov = await provisionModules(license.key, {
|
|
346
|
+
config: delivery.config,
|
|
347
|
+
fetchImpl: delivery.fetchImpl,
|
|
348
|
+
signingKeys: delivery.signingKeys,
|
|
349
|
+
sleep: delivery.sleep,
|
|
350
|
+
});
|
|
351
|
+
for (const m of prov.installed) {
|
|
352
|
+
this.logger.info(`Pro module updated to v${m.version} — restart to load.`);
|
|
353
|
+
}
|
|
354
|
+
for (const e of prov.errors) {
|
|
355
|
+
this.logger.warn(`Pro-module freshness check could not provision the ${e.sku} module ` +
|
|
356
|
+
`(staying on the installed version): ${e.message}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
this.logger.warn(`Background Pro-module freshness check failed (staying on the installed version): ` +
|
|
361
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
333
364
|
assertToolsClassified() {
|
|
334
365
|
for (const tool of this.toolRegistry.list()) {
|
|
335
366
|
tierOf(tool.name);
|
|
@@ -485,6 +516,7 @@ export class EditmameiServer {
|
|
|
485
516
|
await this.server.connect(transport);
|
|
486
517
|
void this.session.initialize().catch(() => undefined);
|
|
487
518
|
void this.reprovisionIfModuleSkipped();
|
|
519
|
+
void this.ensureEntitledModuleFresh();
|
|
488
520
|
this.telemetry.start();
|
|
489
521
|
void this.telemetry.flushOutboxOnStartup();
|
|
490
522
|
this.server.onclose = () => {
|
|
@@ -4,8 +4,8 @@ import { readLicense, writeLicense, clearLicense, } from './store.js';
|
|
|
4
4
|
import { PolarLicenseClient, PolarLicenseError } from './polar-client.js';
|
|
5
5
|
import { Logger } from '../utils/logger.js';
|
|
6
6
|
const logger = new Logger('License');
|
|
7
|
-
export const GRACE_MS =
|
|
8
|
-
export const REFRESH_AFTER_MS =
|
|
7
|
+
export const GRACE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
8
|
+
export const REFRESH_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
9
9
|
export const EXPIRED_REFRESH_TIMEOUT_MS = 5_000;
|
|
10
10
|
export const CLOCK_SKEW_TOLERANCE_MS = 24 * 60 * 60 * 1000;
|
|
11
11
|
export function evaluateEntitlement(rec, now) {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { refreshIfStale } from './entitlement.js';
|
|
2
|
-
export
|
|
3
|
-
|
|
2
|
+
export const PING_REFRESH_MIN_INTERVAL_MS = 15 * 60_000;
|
|
3
|
+
export function createPingLicenseRefresher(refresh = refreshIfStale, opts = {}) {
|
|
4
|
+
const now = opts.now ?? Date.now;
|
|
5
|
+
const minIntervalMs = opts.minIntervalMs ?? PING_REFRESH_MIN_INTERVAL_MS;
|
|
6
|
+
let lastAttemptAt = Number.NEGATIVE_INFINITY;
|
|
4
7
|
return () => {
|
|
5
|
-
|
|
8
|
+
const t = now();
|
|
9
|
+
if (t - lastAttemptAt < minIntervalMs)
|
|
6
10
|
return;
|
|
7
|
-
|
|
11
|
+
lastAttemptAt = t;
|
|
8
12
|
try {
|
|
9
13
|
void refresh().catch(() => { });
|
|
10
14
|
}
|
|
@@ -83,28 +83,34 @@ export class MacOSExecutor {
|
|
|
83
83
|
const dir = await TempDir.create('editmamei-mac-');
|
|
84
84
|
try {
|
|
85
85
|
const jsxPath = await dir.write('script.jsx', script);
|
|
86
|
-
const wrapperPath = await dir.write('wrapper.scpt', this.createAppleScriptWrapper(jsxPath));
|
|
87
|
-
const { stdout, stderr } = await runChildWithTimeout('osascript', [wrapperPath], {
|
|
86
|
+
const wrapperPath = await dir.write('wrapper.scpt', this.createAppleScriptWrapper(jsxPath, timeout));
|
|
87
|
+
const { stdout, stderr, exitCode } = await runChildWithTimeout('osascript', [wrapperPath], {
|
|
88
88
|
timeout,
|
|
89
89
|
diagLabel: 'osascript wrapper.scpt',
|
|
90
90
|
});
|
|
91
91
|
if (stderr) {
|
|
92
92
|
this.logger.warn('Script execution warning:', stderr);
|
|
93
93
|
}
|
|
94
|
+
if (exitCode !== 0) {
|
|
95
|
+
throw new Error(`osascript exited with code ${exitCode}: ${stderr.trim() || '(no stderr)'}`);
|
|
96
|
+
}
|
|
94
97
|
return this.parseResult(stdout);
|
|
95
98
|
}
|
|
96
99
|
finally {
|
|
97
100
|
await dir.cleanup();
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
|
-
createAppleScriptWrapper(jsxPath) {
|
|
103
|
+
createAppleScriptWrapper(jsxPath, timeoutMs = 30000) {
|
|
101
104
|
const posixPath = jsxPath.replace(/\\/g, '/');
|
|
102
105
|
if (posixPath.includes('"') || posixPath.includes("'") || posixPath.includes('\n')) {
|
|
103
106
|
throw new Error('jsxPath contains a character that would break AppleScript interpolation');
|
|
104
107
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
const timeoutSeconds = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1, Math.ceil(timeoutMs / 1000)) : 1;
|
|
109
|
+
return `with timeout of ${timeoutSeconds} seconds
|
|
110
|
+
\ttell application "${this.appName}"
|
|
111
|
+
\t\tdo javascript "$.evalFile(decodeURI('${encodeURI(posixPath)}'))"
|
|
112
|
+
\tend tell
|
|
113
|
+
end timeout`;
|
|
108
114
|
}
|
|
109
115
|
parseResult(output) {
|
|
110
116
|
const trimmed = output.trim();
|
|
@@ -87,9 +87,12 @@ export async function runChildWithTimeout(command, args, options) {
|
|
|
87
87
|
});
|
|
88
88
|
child.on('exit', (code, signal) => {
|
|
89
89
|
if (timedOut) {
|
|
90
|
-
settle(() => reject(new Error(`Script execution timeout after ${timeout}ms (${diagLabel}). The child process was killed
|
|
91
|
-
`
|
|
92
|
-
`
|
|
90
|
+
settle(() => reject(new Error(`Script execution timeout after ${timeout}ms (${diagLabel}). The child process was killed, ` +
|
|
91
|
+
`but Photoshop runs as a separate process and may have kept executing — the operation ` +
|
|
92
|
+
`could still have completed. Check Photoshop's actual state before retrying. Common ` +
|
|
93
|
+
`causes: a genuinely slow operation (e.g. a large RAW file's first Camera Raw engine ` +
|
|
94
|
+
`init) exceeding the timeout, or a modal dialog open in Photoshop (license, missing ` +
|
|
95
|
+
`font, GPU init, "Discard?" prompt) — dismiss it if present.`)));
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
98
|
if (overflowed) {
|
|
Binary file
|
|
@@ -495,6 +495,11 @@ const addAdjustmentLayerSchema = {
|
|
|
495
495
|
description: 'Only meaningful when mask_from_selection is true AND there is an active selection. If true, the resulting mask is inverted — so the adjustment affects EVERYTHING OUTSIDE the selection rather than inside. Common idiom: "I selected the sky but want to adjust everything else." Defaults to false.',
|
|
496
496
|
default: false,
|
|
497
497
|
},
|
|
498
|
+
into_active_group: {
|
|
499
|
+
type: 'boolean',
|
|
500
|
+
description: "Photoshop's Mk-AdjL descriptor carries no placement target, so with a GROUP active it would natively nest the new layer INSIDE that group. Default false hoists the new layer back out so it lands above the active layer/group as a sibling, matching this tool's documented placement. Pass true to keep the new layer nested inside the active group instead.",
|
|
501
|
+
default: false,
|
|
502
|
+
},
|
|
498
503
|
},
|
|
499
504
|
required: ['type'],
|
|
500
505
|
};
|
|
@@ -660,7 +665,7 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
660
665
|
{
|
|
661
666
|
tool: {
|
|
662
667
|
name: 'ps_add_adjustment_layer',
|
|
663
|
-
description: "Create a non-destructive adjustment layer above the active layer. Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for ALL tonal/color adjustments; the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `photoshop_merge_visible_layers`. Optionally clips the adjustment to only affect the layer directly below it. If a selection is active at call time, the new layer is automatically masked by it (toggle with mask_from_selection / mask_inverted). For destructive ops that don't have an adjustment-layer equivalent in Photoshop (Shadows/Highlights — single-pass shadow/highlight recovery), use `photoshop_apply_shadows_highlights` which auto-duplicates the active layer to keep the original intact. Returns context
|
|
668
|
+
description: "Create a non-destructive adjustment layer above the active layer — hoisted out of the active layer's group by default even though Photoshop's own Mk-AdjL placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting). Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for ALL tonal/color adjustments; the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `photoshop_merge_visible_layers`. Optionally clips the adjustment to only affect the layer directly below it. If a selection is active at call time, the new layer is automatically masked by it (toggle with mask_from_selection / mask_inverted). For destructive ops that don't have an adjustment-layer equivalent in Photoshop (Shadows/Highlights — single-pass shadow/highlight recovery), use `photoshop_apply_shadows_highlights` which auto-duplicates the active layer to keep the original intact. Returns context (the new adjustment layer becomes active) plus parent_path — the actual containing-group chain, so placement is never silent.",
|
|
664
669
|
inputSchema: addAdjustmentLayerSchema,
|
|
665
670
|
outputSchema: {
|
|
666
671
|
type: 'object',
|
|
@@ -677,6 +682,15 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
677
682
|
mask_inverted: { type: 'boolean' },
|
|
678
683
|
mask_inversion_error: { type: ['string', 'null'] },
|
|
679
684
|
clipError: { type: 'string' },
|
|
685
|
+
hoisted: {
|
|
686
|
+
type: 'boolean',
|
|
687
|
+
description: 'True when the new layer 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.',
|
|
688
|
+
},
|
|
689
|
+
parent_path: {
|
|
690
|
+
type: ['array', 'null'],
|
|
691
|
+
items: { type: 'string' },
|
|
692
|
+
description: 'The containing-group name chain (outermost first), empty array at the document root.',
|
|
693
|
+
},
|
|
680
694
|
context: { type: 'object' },
|
|
681
695
|
},
|
|
682
696
|
},
|
|
@@ -785,6 +799,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
785
799
|
const name = args.name;
|
|
786
800
|
const maskFromSelection = args.mask_from_selection ?? true;
|
|
787
801
|
const maskInverted = args.mask_inverted ?? false;
|
|
802
|
+
const intoActiveGroup = args.into_active_group ?? false;
|
|
788
803
|
const params = {};
|
|
789
804
|
for (const key of FORWARDED_KEYS) {
|
|
790
805
|
if (args[key] !== undefined)
|
|
@@ -806,6 +821,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
806
821
|
clip_to_below: clipToBelow,
|
|
807
822
|
mask_from_selection: maskFromSelection,
|
|
808
823
|
mask_inverted: maskInverted,
|
|
824
|
+
into_active_group: intoActiveGroup,
|
|
809
825
|
...params,
|
|
810
826
|
};
|
|
811
827
|
if (name !== undefined)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runScript } from '../utils/run-script.js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
|
+
import { OPEN_DOCUMENT_TIMEOUT_MS, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS, } from '../utils/operation-timeouts.js';
|
|
3
4
|
const createDocumentSchema = {
|
|
4
5
|
type: 'object',
|
|
5
6
|
properties: {
|
|
@@ -375,13 +376,27 @@ async function closeDocument(connection, snippetClient, rawArgs) {
|
|
|
375
376
|
};
|
|
376
377
|
}
|
|
377
378
|
}
|
|
379
|
+
function isScriptTimeoutError(message) {
|
|
380
|
+
return /Script execution timeout|AppleEvent timed out|(?:^|[\s(])-1712(?:[\s)]|$)/i.test(message);
|
|
381
|
+
}
|
|
382
|
+
async function reprobeOpenDocument(connection, snippetClient, filePath) {
|
|
383
|
+
try {
|
|
384
|
+
const probeScript = await snippetClient.build('probeOpenDocument', { filePath });
|
|
385
|
+
const result = (await runScript(connection, probeScript, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS));
|
|
386
|
+
return result.success ? result : null;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
378
392
|
async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
393
|
+
let filePath;
|
|
379
394
|
try {
|
|
380
395
|
const args = validateArgs(openDocumentSchema, rawArgs);
|
|
381
|
-
|
|
396
|
+
filePath = args.file_path;
|
|
382
397
|
const suppressDialogs = args.suppress_dialogs;
|
|
383
398
|
const script = await snippetClient.build('openDocumentPipeline', { filePath, suppressDialogs });
|
|
384
|
-
const result = await runScript(connection, script);
|
|
399
|
+
const result = await runScript(connection, script, OPEN_DOCUMENT_TIMEOUT_MS);
|
|
385
400
|
return {
|
|
386
401
|
content: [
|
|
387
402
|
{
|
|
@@ -393,11 +408,28 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
|
393
408
|
};
|
|
394
409
|
}
|
|
395
410
|
catch (error) {
|
|
411
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
412
|
+
if (filePath !== undefined && isScriptTimeoutError(message)) {
|
|
413
|
+
const reprobed = await reprobeOpenDocument(connection, snippetClient, filePath);
|
|
414
|
+
if (reprobed) {
|
|
415
|
+
return {
|
|
416
|
+
content: [
|
|
417
|
+
{
|
|
418
|
+
type: 'text',
|
|
419
|
+
text: `Document opened (the open exceeded the ${OPEN_DOCUMENT_TIMEOUT_MS}ms budget and was ` +
|
|
420
|
+
`reported as a timeout, but a post-timeout check confirmed it actually completed):\n` +
|
|
421
|
+
JSON.stringify(reprobed, null, 2),
|
|
422
|
+
},
|
|
423
|
+
],
|
|
424
|
+
structuredContent: reprobed,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
396
428
|
return {
|
|
397
429
|
content: [
|
|
398
430
|
{
|
|
399
431
|
type: 'text',
|
|
400
|
-
text: `Error opening document: ${
|
|
432
|
+
text: `Error opening document: ${message}`,
|
|
401
433
|
},
|
|
402
434
|
],
|
|
403
435
|
isError: true,
|
|
@@ -27,6 +27,11 @@ const createGroupSchema = {
|
|
|
27
27
|
items: { type: 'string' },
|
|
28
28
|
description: 'Optional list of existing layer names to move into the new group. The first listed name ends up on top of the group stack. Layers not found are returned in `not_found`.',
|
|
29
29
|
},
|
|
30
|
+
into_active_group: {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: "Photoshop's Mk-layerSection descriptor carries no placement target, so with a GROUP active it would natively nest the new group INSIDE that group. Default false hoists the new group back out so it lands above the active layer/group as a sibling, matching this tool's documented placement. Pass true to keep it nested inside the active group instead.",
|
|
33
|
+
default: false,
|
|
34
|
+
},
|
|
30
35
|
},
|
|
31
36
|
required: ['name'],
|
|
32
37
|
};
|
|
@@ -78,7 +83,7 @@ export function createGroupTools(connection, snippetClient) {
|
|
|
78
83
|
{
|
|
79
84
|
tool: {
|
|
80
85
|
name: 'ps_create_group',
|
|
81
|
-
description:
|
|
86
|
+
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.`,
|
|
82
87
|
inputSchema: createGroupSchema,
|
|
83
88
|
outputSchema: {
|
|
84
89
|
type: 'object',
|
|
@@ -87,6 +92,15 @@ export function createGroupTools(connection, snippetClient) {
|
|
|
87
92
|
groupName: { type: 'string' },
|
|
88
93
|
moved_count: { type: 'number' },
|
|
89
94
|
not_found: { type: 'array', items: { type: 'string' } },
|
|
95
|
+
hoisted: {
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
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.',
|
|
98
|
+
},
|
|
99
|
+
parent_path: {
|
|
100
|
+
type: ['array', 'null'],
|
|
101
|
+
items: { type: 'string' },
|
|
102
|
+
description: 'The containing-group name chain (outermost first), empty array at the document root.',
|
|
103
|
+
},
|
|
90
104
|
context: { type: 'object' },
|
|
91
105
|
},
|
|
92
106
|
},
|
|
@@ -283,7 +297,8 @@ async function createGroup(connection, snippetClient, rawArgs) {
|
|
|
283
297
|
const args = validateArgs(createGroupSchema, rawArgs);
|
|
284
298
|
const name = args.name;
|
|
285
299
|
const layers = args.layers;
|
|
286
|
-
const
|
|
300
|
+
const intoActiveGroup = args.into_active_group ?? false;
|
|
301
|
+
const params = { name, into_active_group: intoActiveGroup };
|
|
287
302
|
if (layers !== undefined)
|
|
288
303
|
params.layerNames = layers;
|
|
289
304
|
const script = await snippetClient.build('createGroup', params);
|
|
@@ -84,6 +84,16 @@ const duplicateLayerSchema = {
|
|
|
84
84
|
},
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
|
+
const copyToNewLayerSchema = {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
into_active_group: {
|
|
91
|
+
type: 'boolean',
|
|
92
|
+
description: "Photoshop's CpTL (Layer via Copy) event carries no placement target, so with a GROUP active it would natively nest the new layer INSIDE that group. Default false hoists the new layer back out so it lands above the active layer/group as a sibling. Pass true to keep it nested inside the active group instead.",
|
|
93
|
+
default: false,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
87
97
|
const addLayerStyleSchema = {
|
|
88
98
|
type: 'object',
|
|
89
99
|
properties: {
|
|
@@ -247,11 +257,20 @@ export function createLayerPropertiesTools(connection, snippetClient) {
|
|
|
247
257
|
outputSchema: {
|
|
248
258
|
type: 'object',
|
|
249
259
|
properties: {
|
|
250
|
-
updated: { type: 'boolean' },
|
|
251
260
|
property: { type: 'string' },
|
|
252
261
|
value: { type: ['number', 'string'] },
|
|
262
|
+
requested: { type: ['number', 'string', 'boolean'] },
|
|
263
|
+
verified: { type: 'boolean' },
|
|
264
|
+
verification_unreadable: {
|
|
265
|
+
type: 'boolean',
|
|
266
|
+
description: 'visibility only: true when the write itself did not throw but the own-flag verification read failed (Action Manager error, or the layer had no readable id) — verified is false, but this is NOT proof the write failed, just that it could not be confirmed.',
|
|
267
|
+
},
|
|
253
268
|
fill_opacity: { type: 'number' },
|
|
269
|
+
requested_fill_opacity: { type: 'number' },
|
|
270
|
+
fill_opacity_verified: { type: 'boolean' },
|
|
254
271
|
opacity: { type: 'number' },
|
|
272
|
+
requested_opacity: { type: ['number', 'null'] },
|
|
273
|
+
opacity_verified: { type: ['boolean', 'null'] },
|
|
255
274
|
visible: { type: 'boolean' },
|
|
256
275
|
locked: { type: 'boolean' },
|
|
257
276
|
name: { type: 'string' },
|
|
@@ -278,6 +297,11 @@ export function createLayerPropertiesTools(connection, snippetClient) {
|
|
|
278
297
|
properties: {
|
|
279
298
|
originalName: { type: 'string' },
|
|
280
299
|
newName: { type: 'string' },
|
|
300
|
+
parent_path: {
|
|
301
|
+
type: ['array', 'null'],
|
|
302
|
+
items: { type: 'string' },
|
|
303
|
+
description: "The containing-group name chain (outermost first), empty array at the document root. layer.duplicate() is parent-preserving by DOM semantics, so this always matches the original layer's placement.",
|
|
304
|
+
},
|
|
281
305
|
context: { type: 'object' },
|
|
282
306
|
},
|
|
283
307
|
},
|
|
@@ -291,8 +315,8 @@ export function createLayerPropertiesTools(connection, snippetClient) {
|
|
|
291
315
|
{
|
|
292
316
|
tool: {
|
|
293
317
|
name: 'ps_copy_to_new_layer',
|
|
294
|
-
description: 'Copy the current selection into a NEW layer above the active one — Photoshop\'s "Layer via Copy" (Ctrl+J). The source layer is left untouched and the new copied layer becomes active. With an active selection only the selected pixels are lifted; with no selection it copies the whole active layer (a plain duplicate). Reach for this to isolate a region for independent transforms or filters (stretch, light rays, a local grade) without altering the source. Returns context so the caller sees the new active layer.',
|
|
295
|
-
inputSchema:
|
|
318
|
+
description: 'Copy the current selection into a NEW layer above the active one — Photoshop\'s "Layer via Copy" (Ctrl+J) — hoisted out of the active layer\'s group by default even though the underlying CpTL event carries no placement target and would otherwise nest the new layer INSIDE that group (pass into_active_group:true to keep that native nesting). The source layer is left untouched and the new copied layer becomes active. With an active selection only the selected pixels are lifted; with no selection it copies the whole active layer (a plain duplicate). Reach for this to isolate a region for independent transforms or filters (stretch, light rays, a local grade) without altering the source. Returns context so the caller sees the new active layer.',
|
|
319
|
+
inputSchema: copyToNewLayerSchema,
|
|
296
320
|
outputSchema: {
|
|
297
321
|
type: 'object',
|
|
298
322
|
properties: {
|
|
@@ -301,6 +325,15 @@ export function createLayerPropertiesTools(connection, snippetClient) {
|
|
|
301
325
|
original_active_layer_name: { type: 'string' },
|
|
302
326
|
layer_count_before: { type: 'number' },
|
|
303
327
|
layer_count_after: { type: 'number' },
|
|
328
|
+
hoisted: {
|
|
329
|
+
type: 'boolean',
|
|
330
|
+
description: 'True when the new layer 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.',
|
|
331
|
+
},
|
|
332
|
+
parent_path: {
|
|
333
|
+
type: ['array', 'null'],
|
|
334
|
+
items: { type: 'string' },
|
|
335
|
+
description: 'The containing-group name chain (outermost first), empty array at the document root.',
|
|
336
|
+
},
|
|
304
337
|
context: { type: 'object' },
|
|
305
338
|
},
|
|
306
339
|
},
|
|
@@ -309,7 +342,7 @@ export function createLayerPropertiesTools(connection, snippetClient) {
|
|
|
309
342
|
idempotentHint: false,
|
|
310
343
|
},
|
|
311
344
|
},
|
|
312
|
-
handler: async () => layerViaCopy(connection, snippetClient),
|
|
345
|
+
handler: async (args) => layerViaCopy(connection, snippetClient, args),
|
|
313
346
|
},
|
|
314
347
|
{
|
|
315
348
|
tool: {
|
|
@@ -493,23 +526,25 @@ async function setLayerOpacity(connection, snippetClient, rawArgs) {
|
|
|
493
526
|
if (opacity === undefined && fillPercent === undefined) {
|
|
494
527
|
throw new Error('set_layer_opacity requires opacity and/or fill_percent.');
|
|
495
528
|
}
|
|
496
|
-
let label;
|
|
497
529
|
let buildParams;
|
|
498
530
|
if (fillPercent !== undefined) {
|
|
499
531
|
buildParams = { fillOpacity: fillPercent };
|
|
500
532
|
if (opacity !== undefined)
|
|
501
533
|
buildParams.opacity = opacity;
|
|
502
|
-
label =
|
|
503
|
-
opacity !== undefined
|
|
504
|
-
? `opacity ${opacity}% + fill ${fillPercent}%`
|
|
505
|
-
: `fill ${fillPercent}%`;
|
|
506
534
|
}
|
|
507
535
|
else {
|
|
508
536
|
buildParams = { opacity };
|
|
509
|
-
label = `opacity ${opacity}%`;
|
|
510
537
|
}
|
|
511
538
|
const script = await snippetClient.build('setLayerOpacity', buildParams);
|
|
512
539
|
const result = await runScript(connection, script);
|
|
540
|
+
const r = result;
|
|
541
|
+
const parts = [];
|
|
542
|
+
const opacityActual = r.value ?? (r.requested_opacity != null ? r.opacity : undefined);
|
|
543
|
+
if (opacityActual !== undefined)
|
|
544
|
+
parts.push(`opacity ${opacityActual}%`);
|
|
545
|
+
if (r.fill_opacity !== undefined)
|
|
546
|
+
parts.push(`fill ${r.fill_opacity}%`);
|
|
547
|
+
const label = parts.length > 0 ? parts.join(' + ') : 'opacity';
|
|
513
548
|
return {
|
|
514
549
|
content: [
|
|
515
550
|
{
|
|
@@ -538,11 +573,13 @@ async function setLayerBlendMode(connection, snippetClient, rawArgs) {
|
|
|
538
573
|
const blendMode = args.blend_mode;
|
|
539
574
|
const script = await snippetClient.build('setLayerBlendMode', { blendMode });
|
|
540
575
|
const result = await runScript(connection, script);
|
|
576
|
+
const r = result;
|
|
577
|
+
const text = r.value !== undefined ? `Layer blend mode set to ${r.value}` : 'Layer blend mode set';
|
|
541
578
|
return {
|
|
542
579
|
content: [
|
|
543
580
|
{
|
|
544
581
|
type: 'text',
|
|
545
|
-
text
|
|
582
|
+
text,
|
|
546
583
|
},
|
|
547
584
|
],
|
|
548
585
|
structuredContent: result,
|
|
@@ -566,11 +603,13 @@ async function setLayerVisibility(connection, snippetClient, rawArgs) {
|
|
|
566
603
|
const visible = args.visible;
|
|
567
604
|
const script = await snippetClient.build('setLayerVisibility', { visible });
|
|
568
605
|
const result = await runScript(connection, script);
|
|
606
|
+
const r = result;
|
|
607
|
+
const text = r.visible !== undefined ? `Layer ${r.visible ? 'shown' : 'hidden'}` : 'Layer visibility set';
|
|
569
608
|
return {
|
|
570
609
|
content: [
|
|
571
610
|
{
|
|
572
611
|
type: 'text',
|
|
573
|
-
text
|
|
612
|
+
text,
|
|
574
613
|
},
|
|
575
614
|
],
|
|
576
615
|
structuredContent: result,
|
|
@@ -594,11 +633,13 @@ async function setLayerLocked(connection, snippetClient, rawArgs) {
|
|
|
594
633
|
const locked = args.locked;
|
|
595
634
|
const script = await snippetClient.build('setLayerLocked', { locked });
|
|
596
635
|
const result = await runScript(connection, script);
|
|
636
|
+
const r = result;
|
|
637
|
+
const text = r.locked !== undefined ? `Layer ${r.locked ? 'locked' : 'unlocked'}` : 'Layer lock state set';
|
|
597
638
|
return {
|
|
598
639
|
content: [
|
|
599
640
|
{
|
|
600
641
|
type: 'text',
|
|
601
|
-
text
|
|
642
|
+
text,
|
|
602
643
|
},
|
|
603
644
|
],
|
|
604
645
|
structuredContent: result,
|
|
@@ -622,11 +663,15 @@ async function renameLayer(connection, snippetClient, rawArgs) {
|
|
|
622
663
|
const name = args.name;
|
|
623
664
|
const script = await snippetClient.build('renameLayer', { newName: name });
|
|
624
665
|
const result = await runScript(connection, script);
|
|
666
|
+
const r = result;
|
|
667
|
+
const text = r.newName !== undefined
|
|
668
|
+
? `Layer renamed to: ${r.newName}\nResult: ${JSON.stringify(result)}`
|
|
669
|
+
: `Layer renamed\nResult: ${JSON.stringify(result)}`;
|
|
625
670
|
return {
|
|
626
671
|
content: [
|
|
627
672
|
{
|
|
628
673
|
type: 'text',
|
|
629
|
-
text
|
|
674
|
+
text,
|
|
630
675
|
},
|
|
631
676
|
],
|
|
632
677
|
structuredContent: result,
|
|
@@ -675,9 +720,13 @@ async function duplicateLayer(connection, snippetClient, rawArgs) {
|
|
|
675
720
|
};
|
|
676
721
|
}
|
|
677
722
|
}
|
|
678
|
-
async function layerViaCopy(connection, snippetClient) {
|
|
723
|
+
async function layerViaCopy(connection, snippetClient, rawArgs) {
|
|
679
724
|
try {
|
|
680
|
-
const
|
|
725
|
+
const args = validateArgs(copyToNewLayerSchema, rawArgs);
|
|
726
|
+
const intoActiveGroup = args.into_active_group ?? false;
|
|
727
|
+
const script = await snippetClient.build('layerViaCopy', {
|
|
728
|
+
into_active_group: intoActiveGroup,
|
|
729
|
+
});
|
|
681
730
|
const result = (await runScript(connection, script));
|
|
682
731
|
return {
|
|
683
732
|
content: [
|
|
@@ -86,6 +86,11 @@ const addFillLayerSchema = {
|
|
|
86
86
|
red: { type: 'integer', description: 'Solid color red (0-255).', minimum: 0, maximum: 255 },
|
|
87
87
|
green: { type: 'integer', description: 'Solid color green (0-255).', minimum: 0, maximum: 255 },
|
|
88
88
|
blue: { type: 'integer', description: 'Solid color blue (0-255).', minimum: 0, maximum: 255 },
|
|
89
|
+
into_active_group: {
|
|
90
|
+
type: 'boolean',
|
|
91
|
+
description: "Photoshop's Mk-contentLayer descriptor carries no placement target, so with a GROUP active it would natively nest the new fill layer INSIDE that group. Default false hoists the new layer back out so it lands above the active layer/group as a sibling. Pass true to keep it nested inside the active group instead.",
|
|
92
|
+
default: false,
|
|
93
|
+
},
|
|
89
94
|
},
|
|
90
95
|
required: ['red', 'green', 'blue'],
|
|
91
96
|
};
|
|
@@ -111,6 +116,11 @@ export function createLayerTools(connection, snippetClient) {
|
|
|
111
116
|
properties: {
|
|
112
117
|
created: { type: 'boolean' },
|
|
113
118
|
layerName: { type: 'string' },
|
|
119
|
+
parent_path: {
|
|
120
|
+
type: ['array', 'null'],
|
|
121
|
+
items: { type: 'string' },
|
|
122
|
+
description: 'The containing-group name chain (outermost first), empty array at the document root.',
|
|
123
|
+
},
|
|
114
124
|
context: { type: 'object' },
|
|
115
125
|
},
|
|
116
126
|
},
|
|
@@ -188,7 +198,7 @@ export function createLayerTools(connection, snippetClient) {
|
|
|
188
198
|
{
|
|
189
199
|
tool: {
|
|
190
200
|
name: 'ps_add_fill_layer',
|
|
191
|
-
description:
|
|
201
|
+
description: "Add a non-destructive SOLID COLOR fill layer (an editable, re-colorable content layer — distinct from ps_fill_layer, which bakes color into the active pixel layer). Hoisted out of the active layer's group by default even though Photoshop's own Mk-contentLayer placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting). The new fill layer becomes active. Use for color washes, base backgrounds, and clipped color overlays. (Gradient and pattern fill types are planned.)",
|
|
192
202
|
inputSchema: addFillLayerSchema,
|
|
193
203
|
outputSchema: {
|
|
194
204
|
type: 'object',
|
|
@@ -197,6 +207,15 @@ export function createLayerTools(connection, snippetClient) {
|
|
|
197
207
|
fill_type: { type: 'string' },
|
|
198
208
|
color: { type: 'object' },
|
|
199
209
|
layer_name: { type: 'string' },
|
|
210
|
+
hoisted: {
|
|
211
|
+
type: 'boolean',
|
|
212
|
+
description: 'True when the new layer 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.',
|
|
213
|
+
},
|
|
214
|
+
parent_path: {
|
|
215
|
+
type: ['array', 'null'],
|
|
216
|
+
items: { type: 'string' },
|
|
217
|
+
description: 'The containing-group name chain (outermost first), empty array at the document root.',
|
|
218
|
+
},
|
|
200
219
|
context: { type: 'object' },
|
|
201
220
|
},
|
|
202
221
|
},
|
|
@@ -359,7 +378,13 @@ async function addFillLayer(connection, snippetClient, rawArgs) {
|
|
|
359
378
|
const red = args.red;
|
|
360
379
|
const green = args.green;
|
|
361
380
|
const blue = args.blue;
|
|
362
|
-
const
|
|
381
|
+
const intoActiveGroup = args.into_active_group ?? false;
|
|
382
|
+
const script = await snippetClient.build('addFillLayer', {
|
|
383
|
+
red,
|
|
384
|
+
green,
|
|
385
|
+
blue,
|
|
386
|
+
into_active_group: intoActiveGroup,
|
|
387
|
+
});
|
|
363
388
|
const result = await runScript(connection, script);
|
|
364
389
|
return {
|
|
365
390
|
content: [
|
|
@@ -4,6 +4,7 @@ import { jsLit, jsNum } from '../utils/jsx.js';
|
|
|
4
4
|
import { runScript } from '../utils/run-script.js';
|
|
5
5
|
import { TempDir, userOwnedTempRoot } from '../utils/temp.js';
|
|
6
6
|
import { validateArgs } from '../utils/validate.js';
|
|
7
|
+
import { ANNOTATED_PREVIEW_TIMEOUT_MS } from '../utils/operation-timeouts.js';
|
|
7
8
|
const COLOR_NAME_HEX = {
|
|
8
9
|
red: '#FF0000',
|
|
9
10
|
blue: '#0066FF',
|
|
@@ -836,7 +837,7 @@ async function getPreview(connection, rawArgs) {
|
|
|
836
837
|
throw e;
|
|
837
838
|
}
|
|
838
839
|
`;
|
|
839
|
-
const execTimeoutMs = annotations.length > 0 ?
|
|
840
|
+
const execTimeoutMs = annotations.length > 0 ? ANNOTATED_PREVIEW_TIMEOUT_MS : undefined;
|
|
840
841
|
const result = (await runScript(connection, script, execTimeoutMs));
|
|
841
842
|
const bytes = await readFile(tempPath);
|
|
842
843
|
const base64 = bytes.toString('base64');
|
|
@@ -4,6 +4,7 @@ import { TempDir } from '../utils/temp.js';
|
|
|
4
4
|
import { validateArgs } from '../utils/validate.js';
|
|
5
5
|
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
6
6
|
import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
7
|
+
import { SELECT_SUBJECT_TIMEOUT_MS, SELECT_SKY_TIMEOUT_MS } from '../utils/operation-timeouts.js';
|
|
7
8
|
export const SELECTION_TYPE_ENUM = ['replace', 'add', 'subtract', 'intersect'];
|
|
8
9
|
export const selectionTypeFragment = {
|
|
9
10
|
type: 'string',
|
|
@@ -778,7 +779,7 @@ async function selectSubject(connection, snippetClient, rawArgs) {
|
|
|
778
779
|
const sampleAllLayers = args.sample_all_layers ?? true;
|
|
779
780
|
const selectionType = normalizeSelectionType(args.selection_type);
|
|
780
781
|
const script = await snippetClient.build('selectSubject', { sampleAllLayers, selectionType });
|
|
781
|
-
const result = await runScript(connection, script,
|
|
782
|
+
const result = await runScript(connection, script, SELECT_SUBJECT_TIMEOUT_MS);
|
|
782
783
|
return {
|
|
783
784
|
content: [{ type: 'text', text: `Select Subject (${selectionType}) complete` }],
|
|
784
785
|
structuredContent: result,
|
|
@@ -802,7 +803,7 @@ async function selectSky(connection, snippetClient, rawArgs) {
|
|
|
802
803
|
const sampleAllLayers = args.sample_all_layers ?? true;
|
|
803
804
|
const selectionType = normalizeSelectionType(args.selection_type);
|
|
804
805
|
const script = await snippetClient.build('selectSky', { sampleAllLayers, selectionType });
|
|
805
|
-
const result = await runScript(connection, script,
|
|
806
|
+
const result = await runScript(connection, script, SELECT_SKY_TIMEOUT_MS);
|
|
806
807
|
return {
|
|
807
808
|
content: [{ type: 'text', text: `Select Sky (${selectionType}) complete` }],
|
|
808
809
|
structuredContent: result,
|
|
@@ -75,6 +75,11 @@ const shapeInputSchema = {
|
|
|
75
75
|
...rgbColorFragment,
|
|
76
76
|
description: 'rectangle/ellipse stroke color when stroke_width>0. RGB 0-255. Default black.',
|
|
77
77
|
},
|
|
78
|
+
into_active_group: {
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
description: "Photoshop's Mk-contentLayer descriptor carries no placement target, so with a GROUP active it would natively nest the new shape layer INSIDE that group. Default false hoists the new layer back out so it lands above the active layer/group as a sibling. Pass true to keep it nested inside the active group instead.",
|
|
81
|
+
default: false,
|
|
82
|
+
},
|
|
78
83
|
},
|
|
79
84
|
required: ['type'],
|
|
80
85
|
};
|
|
@@ -83,7 +88,7 @@ export function createShapeTools(connection, snippetClient, client = new OnnxLan
|
|
|
83
88
|
{
|
|
84
89
|
tool: {
|
|
85
90
|
name: 'ps_shape',
|
|
86
|
-
description:
|
|
91
|
+
description: "Draw a vector SHAPE layer — `rectangle` (optionally rounded via corner_radius), `ellipse`, or `line` — filled with a solid color, optionally stroked. Hoisted out of the active layer's group by default even though the underlying Mk-contentLayer descriptor carries no placement target and would otherwise nest the new layer INSIDE that group (pass into_active_group:true to keep that native nesting). Aim it EITHER by anchor-relational `placement` (preferred: name anchors + a relation and the resolver computes the geometry, verified by an objective gate — no pixel-guessing; rectangle/ellipse ← a region relation, line ← a path relation) OR by ABSOLUTE document pixels (top-left origin: rectangle/ellipse take left/top/right/bottom; line takes start_x/start_y → end_x/end_y plus weight — you must know the pixel positions, so prefer the anchor-relational `placement` path above and verify the result with a preview). Creates a new vector layer (non-destructive — delete it to remove). (AM-only; verified live on PS 27.2.0.)",
|
|
87
92
|
inputSchema: shapeInputSchema,
|
|
88
93
|
outputSchema: {
|
|
89
94
|
type: 'object',
|
|
@@ -92,6 +97,15 @@ export function createShapeTools(connection, snippetClient, client = new OnnxLan
|
|
|
92
97
|
shape_type: { type: 'string' },
|
|
93
98
|
layer_name: { type: 'string' },
|
|
94
99
|
stroked: { type: 'boolean' },
|
|
100
|
+
hoisted: {
|
|
101
|
+
type: 'boolean',
|
|
102
|
+
description: 'True when the new layer 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.',
|
|
103
|
+
},
|
|
104
|
+
parent_path: {
|
|
105
|
+
type: ['array', 'null'],
|
|
106
|
+
items: { type: 'string' },
|
|
107
|
+
description: 'The containing-group name chain (outermost first), empty array at the document root.',
|
|
108
|
+
},
|
|
95
109
|
placement: {
|
|
96
110
|
type: 'object',
|
|
97
111
|
description: 'Present when anchor-relational placement was used: the resolved geometry + gate verdict.',
|
|
@@ -174,6 +188,7 @@ async function createShape(connection, snippetClient, client, rawArgs) {
|
|
|
174
188
|
else
|
|
175
189
|
params.weight = args.weight ?? 4;
|
|
176
190
|
colorParams(args.fill_color, 'fill', params);
|
|
191
|
+
params.into_active_group = args.into_active_group ?? false;
|
|
177
192
|
if (type !== 'line') {
|
|
178
193
|
params.strokeWidth = args.stroke_width ?? 0;
|
|
179
194
|
colorParams(args.stroke_color, 'stroke', params);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const OPEN_DOCUMENT_TIMEOUT_MS = 120_000;
|
|
2
|
+
export const OPEN_DOCUMENT_REPROBE_TIMEOUT_MS = 10_000;
|
|
3
|
+
export const CAMERA_RAW_FILTER_TIMEOUT_MS = 120_000;
|
|
4
|
+
export const SELECT_SUBJECT_TIMEOUT_MS = 120_000;
|
|
5
|
+
export const SELECT_SKY_TIMEOUT_MS = 120_000;
|
|
6
|
+
export const ANNOTATED_PREVIEW_TIMEOUT_MS = 90_000;
|
|
@@ -24,6 +24,7 @@ export const ERROR_CLASS_TABLE = [
|
|
|
24
24
|
},
|
|
25
25
|
{ errorClass: 'layer_not_found', pattern: /layer .* not found|no layer named/i },
|
|
26
26
|
{ errorClass: 'ps_command_unavailable', pattern: /not currently available/i },
|
|
27
|
+
{ errorClass: 'timeout', pattern: /timed? ?out|Script execution timeout|exceeded.*bytes/i },
|
|
27
28
|
{
|
|
28
29
|
errorClass: 'ps_modal_blocking',
|
|
29
30
|
pattern: /modal.*dialog|dialog.*blocking|blocked.*modal|photoshop.*modal/i,
|
|
@@ -32,7 +33,6 @@ export const ERROR_CLASS_TABLE = [
|
|
|
32
33
|
errorClass: 'ps_not_running',
|
|
33
34
|
pattern: /CreateObject|photoshop.*not.*running|cannot connect.*photoshop|connection.*failed/i,
|
|
34
35
|
},
|
|
35
|
-
{ errorClass: 'timeout', pattern: /timed? ?out|Script execution timeout|exceeded.*bytes/i },
|
|
36
36
|
];
|
|
37
37
|
export function classifyError(error) {
|
|
38
38
|
if (error === undefined)
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.23.0';
|