editmamei 0.22.4 → 0.24.1
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 +8 -6
- package/dist/api/extendscript/_helpers.js +97 -13
- package/dist/api/photoshop-api.js +4 -3
- package/dist/api/snippet-client.js +38 -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/core/server.js +56 -180
- package/dist/delivery/client.js +50 -1
- package/dist/delivery/provision.js +18 -1
- package/dist/delivery/signing.js +38 -1
- package/dist/delivery/store.js +149 -16
- package/dist/detection/detect-active-doc.js +41 -10
- package/dist/detection/detection-client.js +3 -3
- package/dist/detection/face-detector.js +2 -2
- package/dist/detection/geometry.js +14 -0
- package/dist/detection/landmark-detection-client.js +3 -3
- package/dist/detection/mesh-face.js +2 -3
- package/dist/detection/object-detector.js +2 -2
- package/dist/detection/runtime.js +14 -3
- package/dist/detection/sam-segmenter.js +69 -26
- package/dist/kernel/kernel.js +5 -9
- package/dist/kernel/module-lifecycle.js +181 -0
- package/dist/license/entitlement.js +13 -3
- package/dist/license/store.js +8 -1
- package/dist/modules/ce/index.js +1 -1
- package/dist/perception/facets.js +19 -1
- package/dist/perception/grounding-anchors.js +2 -12
- package/dist/perception/grounding-locate.js +1 -9
- package/dist/perception/grounding-review-crop.js +4 -29
- package/dist/perception/overlay.js +62 -0
- package/dist/perception/pixel-identity.js +41 -0
- package/dist/perception/region-precompute.js +85 -17
- package/dist/perception/scene-model.js +93 -50
- package/dist/perception/select-recipes.js +107 -66
- package/dist/perception/sky-ground-flood.js +0 -5
- package/dist/perception/sky-mask-transfer.js +72 -13
- package/dist/platform/connection.js +42 -11
- package/dist/platform/executor-queue.js +68 -0
- package/dist/platform/macos-detector.js +2 -1
- package/dist/platform/macos-executor.js +18 -67
- package/dist/platform/run-child.js +6 -3
- package/dist/platform/windows-detector.js +14 -7
- package/dist/platform/windows-executor.js +9 -64
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +78 -130
- package/dist/tools/brush-tools.js +3 -14
- package/dist/tools/channel-compose-tools.js +4 -23
- package/dist/tools/detection-tools.js +59 -47
- package/dist/tools/document-tools.js +121 -131
- package/dist/tools/filter-tools.js +489 -730
- package/dist/tools/group-tools.js +90 -156
- package/dist/tools/guide-tools.js +52 -83
- package/dist/tools/history-tools.js +29 -63
- package/dist/tools/image-placement-tools.js +18 -37
- package/dist/tools/image-tools.js +23 -51
- package/dist/tools/layer-ordering-tools.js +2 -9
- package/dist/tools/layer-properties-tools.js +245 -384
- package/dist/tools/layer-tools.js +115 -181
- package/dist/tools/layer-transform-tools.js +41 -106
- package/dist/tools/metadata-tools.js +2 -9
- package/dist/tools/path-tools.js +3 -14
- package/dist/tools/portrait-tools.js +2 -5
- package/dist/tools/preview-tools.js +53 -65
- package/dist/tools/retouch-tools.js +32 -70
- package/dist/tools/scene-tools.js +94 -73
- package/dist/tools/selection-tools.js +306 -563
- package/dist/tools/shape-tools.js +18 -10
- package/dist/tools/text-tools.js +24 -57
- package/dist/tools/transform-canvas-tools.js +21 -54
- package/dist/tools/vector-mask-tools.js +2 -9
- package/dist/utils/jpeg-quality.js +1 -0
- package/dist/utils/logger.js +3 -0
- package/dist/utils/operation-timeouts.js +7 -0
- package/dist/utils/run-script.js +7 -2
- package/dist/utils/session-log.js +134 -12
- package/dist/utils/template-storage.js +5 -6
- package/dist/utils/tool-helpers.js +37 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,14 +3,15 @@ import { promisify } from 'util';
|
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
4
|
import { TempDir } from '../utils/temp.js';
|
|
5
5
|
import { runChildWithTimeout } from './run-child.js';
|
|
6
|
+
import { ScriptQueue } from './executor-queue.js';
|
|
6
7
|
const execAsync = promisify(exec);
|
|
7
8
|
export class MacOSExecutor {
|
|
8
9
|
logger;
|
|
9
|
-
|
|
10
|
-
isProcessing = false;
|
|
10
|
+
queue;
|
|
11
11
|
appName = 'Adobe Photoshop 2025';
|
|
12
12
|
constructor() {
|
|
13
13
|
this.logger = new Logger('MacOSExecutor');
|
|
14
|
+
this.queue = new ScriptQueue(this.logger);
|
|
14
15
|
}
|
|
15
16
|
setAppName(appName) {
|
|
16
17
|
if (/["\\\r\n\t]/.test(appName)) {
|
|
@@ -20,91 +21,41 @@ export class MacOSExecutor {
|
|
|
20
21
|
this.logger.debug(`App name set to: ${appName}`);
|
|
21
22
|
}
|
|
22
23
|
async execute(script, timeout = 30000) {
|
|
23
|
-
return
|
|
24
|
-
const task = {
|
|
25
|
-
run: () => this.executeScript(script, timeout),
|
|
26
|
-
resolve,
|
|
27
|
-
reject,
|
|
28
|
-
cancelled: false,
|
|
29
|
-
settled: false,
|
|
30
|
-
timeoutId: null,
|
|
31
|
-
};
|
|
32
|
-
const QUEUE_SLACK_MS = 3500;
|
|
33
|
-
task.timeoutId = setTimeout(() => {
|
|
34
|
-
if (task.settled)
|
|
35
|
-
return;
|
|
36
|
-
task.cancelled = true;
|
|
37
|
-
task.settled = true;
|
|
38
|
-
task.timeoutId = null;
|
|
39
|
-
reject(new Error('Script execution timeout'));
|
|
40
|
-
}, timeout + QUEUE_SLACK_MS);
|
|
41
|
-
this.scriptQueue.push(task);
|
|
42
|
-
void this.processQueue();
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
settleTask(task, fn) {
|
|
46
|
-
if (task.settled)
|
|
47
|
-
return;
|
|
48
|
-
task.settled = true;
|
|
49
|
-
if (task.timeoutId !== null) {
|
|
50
|
-
clearTimeout(task.timeoutId);
|
|
51
|
-
task.timeoutId = null;
|
|
52
|
-
}
|
|
53
|
-
fn();
|
|
54
|
-
}
|
|
55
|
-
async processQueue() {
|
|
56
|
-
if (this.isProcessing)
|
|
57
|
-
return;
|
|
58
|
-
this.isProcessing = true;
|
|
59
|
-
try {
|
|
60
|
-
while (this.scriptQueue.length > 0) {
|
|
61
|
-
const task = this.scriptQueue.shift();
|
|
62
|
-
if (task.cancelled)
|
|
63
|
-
continue;
|
|
64
|
-
try {
|
|
65
|
-
const result = await task.run();
|
|
66
|
-
this.settleTask(task, () => task.resolve(result));
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
if (task.settled) {
|
|
70
|
-
this.logger.error('Script execution failed after settle:', error);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
this.settleTask(task, () => task.reject(error));
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
finally {
|
|
79
|
-
this.isProcessing = false;
|
|
80
|
-
}
|
|
24
|
+
return this.queue.enqueue(() => this.executeScript(script, timeout), timeout);
|
|
81
25
|
}
|
|
82
26
|
async executeScript(script, timeout) {
|
|
83
27
|
const dir = await TempDir.create('editmamei-mac-');
|
|
84
28
|
try {
|
|
85
29
|
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], {
|
|
30
|
+
const wrapperPath = await dir.write('wrapper.scpt', this.createAppleScriptWrapper(jsxPath, timeout));
|
|
31
|
+
const { stdout, stderr, exitCode } = await runChildWithTimeout('osascript', [wrapperPath], {
|
|
88
32
|
timeout,
|
|
89
33
|
diagLabel: 'osascript wrapper.scpt',
|
|
90
34
|
});
|
|
91
35
|
if (stderr) {
|
|
92
36
|
this.logger.warn('Script execution warning:', stderr);
|
|
93
37
|
}
|
|
38
|
+
if (exitCode !== 0) {
|
|
39
|
+
throw new Error(`osascript exited with code ${exitCode}: ${stderr.trim() || '(no stderr)'}`);
|
|
40
|
+
}
|
|
94
41
|
return this.parseResult(stdout);
|
|
95
42
|
}
|
|
96
43
|
finally {
|
|
97
44
|
await dir.cleanup();
|
|
98
45
|
}
|
|
99
46
|
}
|
|
100
|
-
createAppleScriptWrapper(jsxPath) {
|
|
47
|
+
createAppleScriptWrapper(jsxPath, timeoutMs = 30000) {
|
|
101
48
|
const posixPath = jsxPath.replace(/\\/g, '/');
|
|
102
|
-
if (posixPath.includes('"') || posixPath.includes(
|
|
49
|
+
if (posixPath.includes('"') || posixPath.includes('\n')) {
|
|
103
50
|
throw new Error('jsxPath contains a character that would break AppleScript interpolation');
|
|
104
51
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
52
|
+
const timeoutSeconds = Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.max(1, Math.ceil(timeoutMs / 1000)) : 1;
|
|
53
|
+
const encodedPath = encodeURI(posixPath).replace(/'/g, '%27');
|
|
54
|
+
return `with timeout of ${timeoutSeconds} seconds
|
|
55
|
+
\ttell application "${this.appName}"
|
|
56
|
+
\t\tdo javascript "$.evalFile(decodeURI('${encodedPath}'))"
|
|
57
|
+
\tend tell
|
|
58
|
+
end timeout`;
|
|
108
59
|
}
|
|
109
60
|
parseResult(output) {
|
|
110
61
|
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) {
|
|
@@ -26,11 +26,9 @@ export class WindowsDetector {
|
|
|
26
26
|
this.logger.warn('Registry detection failed:', error);
|
|
27
27
|
}
|
|
28
28
|
const commonPaths = this.getCommonPaths();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return info;
|
|
33
|
-
}
|
|
29
|
+
const commonPathInfo = await this.checkCommonPaths(commonPaths);
|
|
30
|
+
if (commonPathInfo)
|
|
31
|
+
return commonPathInfo;
|
|
34
32
|
throw new Error('Photoshop not found on this system');
|
|
35
33
|
}
|
|
36
34
|
async detectFromRegistry() {
|
|
@@ -107,12 +105,21 @@ export class WindowsDetector {
|
|
|
107
105
|
const programFiles = process.env.ProgramFiles || 'C:\\Program Files';
|
|
108
106
|
const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
|
|
109
107
|
const paths = [];
|
|
110
|
-
|
|
108
|
+
const maxYear = Math.max(new Date().getFullYear() + 1, 2026);
|
|
109
|
+
for (let year = maxYear; year >= 2012; year--) {
|
|
111
110
|
paths.push(`${programFiles}\\Adobe\\Adobe Photoshop ${year}\\Photoshop.exe`, `${programFilesX86}\\Adobe\\Adobe Photoshop ${year}\\Photoshop.exe`, `${programFiles}\\Adobe\\Adobe Photoshop CC ${year}\\Photoshop.exe`, `${programFilesX86}\\Adobe\\Adobe Photoshop CC ${year}\\Photoshop.exe`);
|
|
112
111
|
}
|
|
113
112
|
paths.push(`${programFiles}\\Adobe\\Adobe Photoshop CC\\Photoshop.exe`, `${programFiles}\\Adobe\\Photoshop CC\\Photoshop.exe`);
|
|
114
113
|
return paths;
|
|
115
114
|
}
|
|
115
|
+
async checkCommonPaths(paths) {
|
|
116
|
+
const results = await Promise.all(paths.map((path) => this.checkPath(path)));
|
|
117
|
+
const selected = results.find((result) => result !== null) ?? null;
|
|
118
|
+
if (selected) {
|
|
119
|
+
this.logger.info(`Found Photoshop at: ${selected.path}`);
|
|
120
|
+
}
|
|
121
|
+
return selected;
|
|
122
|
+
}
|
|
116
123
|
async checkPath(path) {
|
|
117
124
|
try {
|
|
118
125
|
let cleanPath = path.trim().replace(/^"|"$/g, '');
|
|
@@ -121,7 +128,7 @@ export class WindowsDetector {
|
|
|
121
128
|
}
|
|
122
129
|
await access(cleanPath, constants.F_OK);
|
|
123
130
|
const version = this.extractVersionFromPath(cleanPath);
|
|
124
|
-
this.logger.
|
|
131
|
+
this.logger.debug(`Found Photoshop at: ${cleanPath}`);
|
|
125
132
|
return {
|
|
126
133
|
version,
|
|
127
134
|
path: cleanPath,
|
|
@@ -4,73 +4,17 @@ import { exec } from 'child_process';
|
|
|
4
4
|
import { Logger } from '../utils/logger.js';
|
|
5
5
|
import { TempDir } from '../utils/temp.js';
|
|
6
6
|
import { runChildWithTimeout } from './run-child.js';
|
|
7
|
+
import { ScriptQueue } from './executor-queue.js';
|
|
7
8
|
const execAsync = promisify(exec);
|
|
8
9
|
export class WindowsExecutor {
|
|
9
10
|
logger;
|
|
10
|
-
|
|
11
|
-
isProcessing = false;
|
|
11
|
+
queue;
|
|
12
12
|
constructor() {
|
|
13
13
|
this.logger = new Logger('WindowsExecutor');
|
|
14
|
+
this.queue = new ScriptQueue(this.logger);
|
|
14
15
|
}
|
|
15
16
|
async execute(script, timeout = 30000) {
|
|
16
|
-
return
|
|
17
|
-
const task = {
|
|
18
|
-
run: () => this.executeScript(script, timeout),
|
|
19
|
-
resolve,
|
|
20
|
-
reject,
|
|
21
|
-
cancelled: false,
|
|
22
|
-
settled: false,
|
|
23
|
-
timeoutId: null,
|
|
24
|
-
};
|
|
25
|
-
const QUEUE_SLACK_MS = 3500;
|
|
26
|
-
task.timeoutId = setTimeout(() => {
|
|
27
|
-
if (task.settled)
|
|
28
|
-
return;
|
|
29
|
-
task.cancelled = true;
|
|
30
|
-
task.settled = true;
|
|
31
|
-
task.timeoutId = null;
|
|
32
|
-
reject(new Error('Script execution timeout'));
|
|
33
|
-
}, timeout + QUEUE_SLACK_MS);
|
|
34
|
-
this.scriptQueue.push(task);
|
|
35
|
-
void this.processQueue();
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
settleTask(task, fn) {
|
|
39
|
-
if (task.settled)
|
|
40
|
-
return;
|
|
41
|
-
task.settled = true;
|
|
42
|
-
if (task.timeoutId !== null) {
|
|
43
|
-
clearTimeout(task.timeoutId);
|
|
44
|
-
task.timeoutId = null;
|
|
45
|
-
}
|
|
46
|
-
fn();
|
|
47
|
-
}
|
|
48
|
-
async processQueue() {
|
|
49
|
-
if (this.isProcessing)
|
|
50
|
-
return;
|
|
51
|
-
this.isProcessing = true;
|
|
52
|
-
try {
|
|
53
|
-
while (this.scriptQueue.length > 0) {
|
|
54
|
-
const task = this.scriptQueue.shift();
|
|
55
|
-
if (task.cancelled)
|
|
56
|
-
continue;
|
|
57
|
-
try {
|
|
58
|
-
const result = await task.run();
|
|
59
|
-
this.settleTask(task, () => task.resolve(result));
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
if (task.settled) {
|
|
63
|
-
this.logger.error('Script execution failed after settle:', error);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
this.settleTask(task, () => task.reject(error));
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
finally {
|
|
72
|
-
this.isProcessing = false;
|
|
73
|
-
}
|
|
17
|
+
return this.queue.enqueue(() => this.executeScript(script, timeout), timeout);
|
|
74
18
|
}
|
|
75
19
|
async executeScript(script, timeout) {
|
|
76
20
|
const dir = await TempDir.create('editmamei-win-');
|
|
@@ -91,8 +35,8 @@ export class WindowsExecutor {
|
|
|
91
35
|
}
|
|
92
36
|
}
|
|
93
37
|
createVBSWrapper(jsxPath) {
|
|
94
|
-
if (jsxPath.includes('"')
|
|
95
|
-
throw new Error('jsxPath contains a quote
|
|
38
|
+
if (jsxPath.includes('"')) {
|
|
39
|
+
throw new Error('jsxPath contains a double-quote — refusing to compose VBS');
|
|
96
40
|
}
|
|
97
41
|
return `
|
|
98
42
|
On Error Resume Next
|
|
@@ -104,8 +48,9 @@ If Err.Number <> 0 Then
|
|
|
104
48
|
WScript.Quit 1
|
|
105
49
|
End If
|
|
106
50
|
|
|
107
|
-
' Execute the JSX script. The path is the mkdtemp temp file (quote-free
|
|
108
|
-
' guard above), but its single backslashes
|
|
51
|
+
' Execute the JSX script. The path is the mkdtemp temp file (double-quote-free
|
|
52
|
+
' by the guard above; apostrophes are fine here), but its single backslashes
|
|
53
|
+
' must be DOUBLED here: this string is
|
|
109
54
|
' handed to DoJavaScript as ExtendScript SOURCE, and a backslash inside a quoted
|
|
110
55
|
' path there is a JS string-literal escape (a Users/Temp segment would be parsed
|
|
111
56
|
' as an escape sequence and corrupt the path). The VBS Replace doubles each
|
|
Binary file
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runScript } from '../utils/run-script.js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
|
+
import { toolErrorResult, runSnippetTool, applyToActiveLayerProp } from '../utils/tool-helpers.js';
|
|
3
4
|
const addAdjustmentLayerSchema = {
|
|
4
5
|
type: 'object',
|
|
5
6
|
properties: {
|
|
@@ -495,6 +496,11 @@ const addAdjustmentLayerSchema = {
|
|
|
495
496
|
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
497
|
default: false,
|
|
497
498
|
},
|
|
499
|
+
into_active_group: {
|
|
500
|
+
type: 'boolean',
|
|
501
|
+
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.",
|
|
502
|
+
default: false,
|
|
503
|
+
},
|
|
498
504
|
},
|
|
499
505
|
required: ['type'],
|
|
500
506
|
};
|
|
@@ -505,22 +511,14 @@ const colorLookupBakeSchema = {
|
|
|
505
511
|
type: 'string',
|
|
506
512
|
description: 'Leaf name of a 3DLUT file in PS\'s Presets/3DLUTs/ folder (e.g. "TealOrangePlusContrast.3DL", "FallColors.look", "Kodak 5205 Fuji 3510 (by Adobe).cube") OR an absolute path to a .3DL / .look / .cube file. Extension-agnostic leaf-name resolution (the same base name with a different extension still matches).',
|
|
507
513
|
},
|
|
508
|
-
apply_to_active_layer:
|
|
509
|
-
type: 'boolean',
|
|
510
|
-
description: 'If false (default), runs on a DUPLICATE of the active layer; the original is preserved. If true, bakes into the active layer in place.',
|
|
511
|
-
default: false,
|
|
512
|
-
},
|
|
514
|
+
apply_to_active_layer: applyToActiveLayerProp('the color lookup'),
|
|
513
515
|
},
|
|
514
516
|
required: ['cl_lut_name'],
|
|
515
517
|
};
|
|
516
518
|
const equalizeSchema = {
|
|
517
519
|
type: 'object',
|
|
518
520
|
properties: {
|
|
519
|
-
apply_to_active_layer:
|
|
520
|
-
type: 'boolean',
|
|
521
|
-
description: 'If false (default), the operation runs on a DUPLICATE of the active layer and the original is preserved. If true, bakes into the active layer in place. Use false unless you specifically want the destructive-in-place behavior.',
|
|
522
|
-
default: false,
|
|
523
|
-
},
|
|
521
|
+
apply_to_active_layer: applyToActiveLayerProp('the equalize'),
|
|
524
522
|
},
|
|
525
523
|
};
|
|
526
524
|
const shadowsHighlightsSchema = {
|
|
@@ -596,11 +594,7 @@ const shadowsHighlightsSchema = {
|
|
|
596
594
|
maximum: 50,
|
|
597
595
|
default: 0.01,
|
|
598
596
|
},
|
|
599
|
-
apply_to_active_layer:
|
|
600
|
-
type: 'boolean',
|
|
601
|
-
description: 'If false (default), the op is applied to a duplicate of the active layer named "Shadows/Highlights (<Original Name>)" — original preserved. If true, bakes directly into the active layer.',
|
|
602
|
-
default: false,
|
|
603
|
-
},
|
|
597
|
+
apply_to_active_layer: applyToActiveLayerProp('the op'),
|
|
604
598
|
},
|
|
605
599
|
};
|
|
606
600
|
const APPLY_ADJUSTMENT_TYPES = ['shadows_highlights', 'equalize', 'color_lookup'];
|
|
@@ -660,7 +654,7 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
660
654
|
{
|
|
661
655
|
tool: {
|
|
662
656
|
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 `
|
|
657
|
+
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 `ps_merge` (mode=visible). 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 `ps_apply_adjustment` (type=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
658
|
inputSchema: addAdjustmentLayerSchema,
|
|
665
659
|
outputSchema: {
|
|
666
660
|
type: 'object',
|
|
@@ -677,6 +671,15 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
677
671
|
mask_inverted: { type: 'boolean' },
|
|
678
672
|
mask_inversion_error: { type: ['string', 'null'] },
|
|
679
673
|
clipError: { type: 'string' },
|
|
674
|
+
hoisted: {
|
|
675
|
+
type: 'boolean',
|
|
676
|
+
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.',
|
|
677
|
+
},
|
|
678
|
+
parent_path: {
|
|
679
|
+
type: ['array', 'null'],
|
|
680
|
+
items: { type: 'string' },
|
|
681
|
+
description: 'The containing-group name chain (outermost first), empty array at the document root.',
|
|
682
|
+
},
|
|
680
683
|
context: { type: 'object' },
|
|
681
684
|
},
|
|
682
685
|
},
|
|
@@ -785,6 +788,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
785
788
|
const name = args.name;
|
|
786
789
|
const maskFromSelection = args.mask_from_selection ?? true;
|
|
787
790
|
const maskInverted = args.mask_inverted ?? false;
|
|
791
|
+
const intoActiveGroup = args.into_active_group ?? false;
|
|
788
792
|
const params = {};
|
|
789
793
|
for (const key of FORWARDED_KEYS) {
|
|
790
794
|
if (args[key] !== undefined)
|
|
@@ -806,6 +810,7 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
806
810
|
clip_to_below: clipToBelow,
|
|
807
811
|
mask_from_selection: maskFromSelection,
|
|
808
812
|
mask_inverted: maskInverted,
|
|
813
|
+
into_active_group: intoActiveGroup,
|
|
809
814
|
...params,
|
|
810
815
|
};
|
|
811
816
|
if (name !== undefined)
|
|
@@ -823,125 +828,68 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
823
828
|
};
|
|
824
829
|
}
|
|
825
830
|
catch (error) {
|
|
826
|
-
return
|
|
827
|
-
content: [
|
|
828
|
-
{
|
|
829
|
-
type: 'text',
|
|
830
|
-
text: `Error adding adjustment layer: ${error instanceof Error ? error.message : String(error)}`,
|
|
831
|
-
},
|
|
832
|
-
],
|
|
833
|
-
isError: true,
|
|
834
|
-
};
|
|
831
|
+
return toolErrorResult('Error adding adjustment layer', error);
|
|
835
832
|
}
|
|
836
833
|
}
|
|
837
834
|
async function applyColorLookup(connection, snippetClient, rawArgs) {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
structuredContent: result,
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
catch (error) {
|
|
855
|
-
return {
|
|
856
|
-
content: [
|
|
857
|
-
{
|
|
858
|
-
type: 'text',
|
|
859
|
-
text: `Error applying color lookup: ${error instanceof Error ? error.message : String(error)}`,
|
|
860
|
-
},
|
|
861
|
-
],
|
|
862
|
-
isError: true,
|
|
863
|
-
};
|
|
864
|
-
}
|
|
835
|
+
return runSnippetTool({
|
|
836
|
+
connection,
|
|
837
|
+
snippetClient,
|
|
838
|
+
rawArgs,
|
|
839
|
+
schema: colorLookupBakeSchema,
|
|
840
|
+
snippet: 'applyColorLookup',
|
|
841
|
+
errorPrefix: 'Error applying color lookup',
|
|
842
|
+
params: (args) => ({
|
|
843
|
+
lutName: args.cl_lut_name,
|
|
844
|
+
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
845
|
+
}),
|
|
846
|
+
successText: (result) => `Color Lookup applied (BAKE, experimental):\n${JSON.stringify(result, null, 2)}`,
|
|
847
|
+
});
|
|
865
848
|
}
|
|
866
849
|
async function applyEqualize(connection, snippetClient, rawArgs) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
structuredContent: result,
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
catch (error) {
|
|
883
|
-
return {
|
|
884
|
-
content: [
|
|
885
|
-
{
|
|
886
|
-
type: 'text',
|
|
887
|
-
text: `Error applying equalize: ${error instanceof Error ? error.message : String(error)}`,
|
|
888
|
-
},
|
|
889
|
-
],
|
|
890
|
-
isError: true,
|
|
891
|
-
};
|
|
892
|
-
}
|
|
850
|
+
return runSnippetTool({
|
|
851
|
+
connection,
|
|
852
|
+
snippetClient,
|
|
853
|
+
rawArgs,
|
|
854
|
+
schema: equalizeSchema,
|
|
855
|
+
snippet: 'applyEqualize',
|
|
856
|
+
errorPrefix: 'Error applying equalize',
|
|
857
|
+
params: (args) => ({
|
|
858
|
+
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
859
|
+
}),
|
|
860
|
+
successText: (result) => `Equalize applied:\n${JSON.stringify(result, null, 2)}`,
|
|
861
|
+
});
|
|
893
862
|
}
|
|
894
863
|
async function applyShadowsHighlights(connection, snippetClient, rawArgs) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
return {
|
|
927
|
-
content: [
|
|
928
|
-
{
|
|
929
|
-
type: 'text',
|
|
930
|
-
text: `Shadows/Highlights applied to ${target} (shadow ${shadowAmount}, highlight ${highlightAmount}, color ${colorCorrection}).`,
|
|
931
|
-
},
|
|
932
|
-
],
|
|
933
|
-
structuredContent: result,
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
catch (error) {
|
|
937
|
-
return {
|
|
938
|
-
content: [
|
|
939
|
-
{
|
|
940
|
-
type: 'text',
|
|
941
|
-
text: `Error applying shadows/highlights: ${error instanceof Error ? error.message : String(error)}`,
|
|
942
|
-
},
|
|
943
|
-
],
|
|
944
|
-
isError: true,
|
|
945
|
-
};
|
|
946
|
-
}
|
|
864
|
+
return runSnippetTool({
|
|
865
|
+
connection,
|
|
866
|
+
snippetClient,
|
|
867
|
+
rawArgs,
|
|
868
|
+
schema: shadowsHighlightsSchema,
|
|
869
|
+
snippet: 'applyShadowsHighlights',
|
|
870
|
+
errorPrefix: 'Error applying shadows/highlights',
|
|
871
|
+
params: (args) => ({
|
|
872
|
+
shadowAmount: args.shadow_amount ?? 35,
|
|
873
|
+
shadowWidth: args.shadow_width ?? 50,
|
|
874
|
+
shadowRadius: args.shadow_radius ?? 30,
|
|
875
|
+
highlightAmount: args.highlight_amount ?? 0,
|
|
876
|
+
highlightWidth: args.highlight_width ?? 50,
|
|
877
|
+
highlightRadius: args.highlight_radius ?? 30,
|
|
878
|
+
colorCorrection: args.color_correction ?? 20,
|
|
879
|
+
midtoneContrast: args.midtone_contrast ?? 0,
|
|
880
|
+
blackClip: args.black_clip ?? 0.01,
|
|
881
|
+
whiteClip: args.white_clip ?? 0.01,
|
|
882
|
+
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
883
|
+
}),
|
|
884
|
+
successText: (result, args) => {
|
|
885
|
+
const r = result;
|
|
886
|
+
const target = r.target_was_copy
|
|
887
|
+
? `new copy "${r.target_layer_name ?? '?'}"`
|
|
888
|
+
: 'active layer (in place)';
|
|
889
|
+
const shadowAmount = args.shadow_amount ?? 35;
|
|
890
|
+
const highlightAmount = args.highlight_amount ?? 0;
|
|
891
|
+
const colorCorrection = args.color_correction ?? 20;
|
|
892
|
+
return `Shadows/Highlights applied to ${target} (shadow ${shadowAmount}, highlight ${highlightAmount}, color ${colorCorrection}).`;
|
|
893
|
+
},
|
|
894
|
+
});
|
|
947
895
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SUPPORTED_BRUSH_TOOLS } from '../api/brush-tool-names.js';
|
|
2
2
|
import { runScript } from '../utils/run-script.js';
|
|
3
3
|
import { validateArgs } from '../utils/validate.js';
|
|
4
|
+
import { toolErrorResult, applyToActiveLayerProp } from '../utils/tool-helpers.js';
|
|
4
5
|
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
5
6
|
import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
6
7
|
const TOOLS_REQUIRING_SOURCE = ['clone_stamp', 'healing_brush'];
|
|
@@ -109,11 +110,7 @@ const applyBrushStrokeSchema = {
|
|
|
109
110
|
maximum: 50,
|
|
110
111
|
default: 0,
|
|
111
112
|
},
|
|
112
|
-
apply_to_active_layer:
|
|
113
|
-
type: 'boolean',
|
|
114
|
-
description: 'If false (default), the stroke runs on a duplicate named "Brush Stroke (<original name>)" — the original is preserved, undo is delete-the-copy (Bundle O pattern). If true, the stroke bakes into the active layer directly.',
|
|
115
|
-
default: false,
|
|
116
|
-
},
|
|
113
|
+
apply_to_active_layer: applyToActiveLayerProp('the stroke'),
|
|
117
114
|
},
|
|
118
115
|
required: ['tool', 'brush_size'],
|
|
119
116
|
};
|
|
@@ -292,15 +289,7 @@ async function applyBrushStroke(connection, snippetClient, client, rawArgs) {
|
|
|
292
289
|
};
|
|
293
290
|
}
|
|
294
291
|
catch (error) {
|
|
295
|
-
return
|
|
296
|
-
content: [
|
|
297
|
-
{
|
|
298
|
-
type: 'text',
|
|
299
|
-
text: `Error applying brush stroke: ${error instanceof Error ? error.message : String(error)}`,
|
|
300
|
-
},
|
|
301
|
-
],
|
|
302
|
-
isError: true,
|
|
303
|
-
};
|
|
292
|
+
return toolErrorResult('Error applying brush stroke', error);
|
|
304
293
|
}
|
|
305
294
|
}
|
|
306
295
|
function deterministicNoise(i, anchorSeed, axisSalt) {
|