editmamei 1.7.1 → 1.9.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 +3 -3
- package/dist/backends/gimp/bridge/lib.py +1790 -26
- package/dist/backends/gimp/bridge/ops.py +3013 -168
- 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/config.js +7 -0
- package/dist/core/server.js +58 -13
- package/dist/core/settings.js +7 -0
- package/dist/core/tool-activity.js +15 -1
- package/dist/core/tool-groups.js +9 -1
- package/dist/core/tool-tiers.js +9 -1
- package/dist/delivery/client.js +3 -0
- package/dist/modules/gimp/index.js +12 -2
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/sanitize.js +15 -1
- package/dist/tools/adjustment-tools.js +9 -1
- package/dist/tools/document-tools.js +118 -21
- package/dist/tools/filter-tools.js +2 -1
- package/dist/tools/gimp-adjustment-tools.js +7 -6
- package/dist/tools/gimp-checkpoint-tools.js +7 -1
- package/dist/tools/gimp-core-tools.js +10 -11
- package/dist/tools/gimp-document-tools.js +100 -18
- package/dist/tools/gimp-effect-tools.js +56 -10
- package/dist/tools/gimp-fill-tools.js +256 -0
- package/dist/tools/gimp-filter-tools.js +4 -1
- package/dist/tools/gimp-geometry-tools.js +2 -2
- package/dist/tools/gimp-inspect-tools.js +48 -10
- package/dist/tools/gimp-layer-tools.js +7 -24
- package/dist/tools/gimp-match-tools.js +133 -0
- package/dist/tools/gimp-result-text.js +104 -0
- package/dist/tools/gimp-retouch-tools.js +238 -0
- package/dist/tools/gimp-selection-tools.js +374 -0
- package/dist/tools/gimp-shared.js +12 -0
- package/dist/tools/gimp-text-tools.js +241 -0
- package/dist/tools/gimp-transform-layer-tools.js +215 -0
- package/dist/tools/gimp-verify-tools.js +13 -15
- package/dist/tools/image-placement-tools.js +6 -1
- package/dist/tools/sky-tools.js +2 -1
- package/dist/utils/gimp-path.js +18 -22
- package/dist/utils/local-path.js +99 -0
- package/dist/utils/operation-timeouts.js +12 -2
- package/dist/version.js +1 -1
- package/package.json +5 -5
- package/dist/tools/gimp-mask-tools.js +0 -102
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/config.js
CHANGED
|
@@ -50,6 +50,13 @@ const KEYS = {
|
|
|
50
50
|
},
|
|
51
51
|
coerce: coerceBool,
|
|
52
52
|
},
|
|
53
|
+
'automation.allow_execute_script': {
|
|
54
|
+
get: (s) => s.automation.allow_execute_script,
|
|
55
|
+
set: (s, v) => {
|
|
56
|
+
s.automation.allow_execute_script = v;
|
|
57
|
+
},
|
|
58
|
+
coerce: coerceBool,
|
|
59
|
+
},
|
|
53
60
|
ps_path: {
|
|
54
61
|
get: (s) => s.ps_path,
|
|
55
62
|
set: (s, v) => {
|
package/dist/core/server.js
CHANGED
|
@@ -66,6 +66,14 @@ export const FIRST_RUN_DISCLOSURE = 'First run: Editmamei collects content-free
|
|
|
66
66
|
'`editmamei config set telemetry.usage false` (or edit ~/.editmamei/settings.json). ' +
|
|
67
67
|
'Opt in to sanitized diagnostics: `editmamei config set telemetry.diagnostics true`.';
|
|
68
68
|
export const BACKGROUND_VERSION_PROBE_TIMEOUT_MS = 3_000;
|
|
69
|
+
const PING_ROUND_TRIP_CLASSES = new Set([
|
|
70
|
+
'timeout',
|
|
71
|
+
'ps_modal_blocking',
|
|
72
|
+
'ps_not_running',
|
|
73
|
+
'ps_general_error',
|
|
74
|
+
'file_io',
|
|
75
|
+
]);
|
|
76
|
+
const PHOTOSHOP_PATH_DOCS_URL = 'https://github.com/editmamei/editmamei/blob/main/docs/installation.md#optional-pin-a-specific-photoshop-install';
|
|
69
77
|
export class EditmameiServer {
|
|
70
78
|
server;
|
|
71
79
|
logger;
|
|
@@ -83,6 +91,7 @@ export class EditmameiServer {
|
|
|
83
91
|
}
|
|
84
92
|
psVersion = null;
|
|
85
93
|
lastPingReachedPs = null;
|
|
94
|
+
lastPingFailure = null;
|
|
86
95
|
lastTelemetryCallKeyHash = null;
|
|
87
96
|
lastPsLocale = null;
|
|
88
97
|
lastDocDepth = null;
|
|
@@ -169,8 +178,13 @@ export class EditmameiServer {
|
|
|
169
178
|
});
|
|
170
179
|
const telemetrySuccess = entry.tool === 'ps_ping' && this.lastPingReachedPs === false ? false : entry.success;
|
|
171
180
|
const isPingDowngrade = entry.tool === 'ps_ping' && !telemetrySuccess && entry.error === undefined;
|
|
181
|
+
const pingFailure = isPingDowngrade ? this.lastPingFailure : null;
|
|
172
182
|
const errorClass = classifyError(entry.error) ??
|
|
173
|
-
(telemetrySuccess
|
|
183
|
+
(telemetrySuccess
|
|
184
|
+
? null
|
|
185
|
+
: isPingDowngrade
|
|
186
|
+
? (pingFailure?.errorClass ?? 'ps_not_running')
|
|
187
|
+
: NO_ERROR_TEXT_CLASS);
|
|
174
188
|
const telemetryCallKeyHash = hashRetryKey(entry.tool, entry.args);
|
|
175
189
|
const isTelemetryRetry = telemetryCallKeyHash !== null && telemetryCallKeyHash === this.lastTelemetryCallKeyHash;
|
|
176
190
|
if (telemetryCallKeyHash !== null)
|
|
@@ -189,7 +203,7 @@ export class EditmameiServer {
|
|
|
189
203
|
error_class: errorClass ?? NO_ERROR_TEXT_CLASS,
|
|
190
204
|
error_message: entry.error ||
|
|
191
205
|
(isPingDowngrade
|
|
192
|
-
? 'ps_ping did not reach Photoshop'
|
|
206
|
+
? (pingFailure?.message ?? 'ps_ping did not reach Photoshop')
|
|
193
207
|
: 'tool reported failure with no message'),
|
|
194
208
|
...(this.lastPsLocale !== null ? { ps_locale: this.lastPsLocale } : {}),
|
|
195
209
|
...(this.lastDocDepth !== null ? { doc_depth: this.lastDocDepth } : {}),
|
|
@@ -493,8 +507,15 @@ export class EditmameiServer {
|
|
|
493
507
|
}
|
|
494
508
|
})();
|
|
495
509
|
}
|
|
510
|
+
markPingUnreachable(errorClass, message) {
|
|
511
|
+
this.lastPingReachedPs = false;
|
|
512
|
+
this.lastPingFailure = { errorClass, message };
|
|
513
|
+
this.lastDocDepth = null;
|
|
514
|
+
this.lastDocMode = null;
|
|
515
|
+
}
|
|
496
516
|
async pingPhotoshop() {
|
|
497
517
|
this.lastPingReachedPs = null;
|
|
518
|
+
this.lastPingFailure = null;
|
|
498
519
|
this.refreshLicenseOnPing();
|
|
499
520
|
if (this.updateCheck)
|
|
500
521
|
await this.updateCheck;
|
|
@@ -515,17 +536,41 @@ export class EditmameiServer {
|
|
|
515
536
|
const degraded = [];
|
|
516
537
|
let versionIsLive = false;
|
|
517
538
|
let pingStateObserved = false;
|
|
539
|
+
let detectError = null;
|
|
518
540
|
try {
|
|
519
541
|
const detected = await connection.getVersion();
|
|
520
542
|
if (detected)
|
|
521
543
|
version = detected;
|
|
522
544
|
}
|
|
523
|
-
catch {
|
|
545
|
+
catch (err) {
|
|
546
|
+
detectError = err instanceof Error ? err.message : String(err);
|
|
547
|
+
}
|
|
548
|
+
if (connection.getPhotoshopInfo() === null && unsupportedHostReason() === null) {
|
|
549
|
+
this.markPingUnreachable('ps_not_detected', detectError ?? 'No Photoshop install was detected');
|
|
550
|
+
return {
|
|
551
|
+
content: [
|
|
552
|
+
{
|
|
553
|
+
type: 'text',
|
|
554
|
+
text: withAdvisory('Editmamei could not find a Photoshop install on this computer. Photoshop must be ' +
|
|
555
|
+
'installed on the same computer as Editmamei. ' +
|
|
556
|
+
(process.env.PHOTOSHOP_PATH?.trim()
|
|
557
|
+
? 'PHOTOSHOP_PATH is set, but does not point at a Photoshop install: check it '
|
|
558
|
+
: 'If it is installed in a custom location, set PHOTOSHOP_PATH to its full path ') +
|
|
559
|
+
"in your MCP client's Editmamei config (see " +
|
|
560
|
+
PHOTOSHOP_PATH_DOCS_URL +
|
|
561
|
+
'), restart the client, then call ps_ping again.' +
|
|
562
|
+
update.note),
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
structuredContent: {
|
|
566
|
+
connected: false,
|
|
567
|
+
update_available: this.updateInfo,
|
|
568
|
+
notify_user: update.notify,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
524
571
|
}
|
|
525
572
|
if (connection.getPhotoshopInfo() !== null && !(await connection.isCurrentlyRunning())) {
|
|
526
|
-
this.
|
|
527
|
-
this.lastDocDepth = null;
|
|
528
|
-
this.lastDocMode = null;
|
|
573
|
+
this.markPingUnreachable('ps_not_running', 'Photoshop is installed but no Photoshop process is running');
|
|
529
574
|
return {
|
|
530
575
|
content: [
|
|
531
576
|
{
|
|
@@ -550,9 +595,7 @@ export class EditmameiServer {
|
|
|
550
595
|
const alive = await connection.ping().catch(() => false);
|
|
551
596
|
if (!alive) {
|
|
552
597
|
const reason = unsupportedHostReason();
|
|
553
|
-
this.
|
|
554
|
-
this.lastDocDepth = null;
|
|
555
|
-
this.lastDocMode = null;
|
|
598
|
+
this.markPingUnreachable('ps_not_running', reason ?? 'pingState snippet build failed and the fallback ping did not reach Photoshop');
|
|
556
599
|
return {
|
|
557
600
|
content: [
|
|
558
601
|
{
|
|
@@ -575,10 +618,12 @@ export class EditmameiServer {
|
|
|
575
618
|
state = (await runScript(connection, pingStateSnippet));
|
|
576
619
|
}
|
|
577
620
|
catch (err) {
|
|
578
|
-
|
|
579
|
-
this.
|
|
580
|
-
|
|
581
|
-
this.
|
|
621
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
622
|
+
this.logger.warn(`pingState snippet failed: ${message}`);
|
|
623
|
+
const errorClass = classifyError(message);
|
|
624
|
+
this.markPingUnreachable(errorClass !== null && PING_ROUND_TRIP_CLASSES.has(errorClass)
|
|
625
|
+
? errorClass
|
|
626
|
+
: 'ps_not_running', message);
|
|
582
627
|
return {
|
|
583
628
|
content: [
|
|
584
629
|
{
|
package/dist/core/settings.js
CHANGED
|
@@ -21,6 +21,7 @@ function defaults(installId) {
|
|
|
21
21
|
return {
|
|
22
22
|
telemetry: { usage: true, diagnostics: false, install_id: installId },
|
|
23
23
|
privacy: { send_previews_to_llm: true },
|
|
24
|
+
automation: { allow_execute_script: true },
|
|
24
25
|
ps_path: null,
|
|
25
26
|
update_check: true,
|
|
26
27
|
editor: 'auto',
|
|
@@ -34,6 +35,7 @@ function coerce(raw, installId) {
|
|
|
34
35
|
const r = raw;
|
|
35
36
|
const t = (r.telemetry ?? {});
|
|
36
37
|
const p = (r.privacy ?? {});
|
|
38
|
+
const a = (r.automation ?? {});
|
|
37
39
|
return {
|
|
38
40
|
telemetry: {
|
|
39
41
|
usage: typeof t.usage === 'boolean' ? t.usage : base.telemetry.usage,
|
|
@@ -47,6 +49,11 @@ function coerce(raw, installId) {
|
|
|
47
49
|
? p.send_previews_to_llm
|
|
48
50
|
: base.privacy.send_previews_to_llm,
|
|
49
51
|
},
|
|
52
|
+
automation: {
|
|
53
|
+
allow_execute_script: typeof a.allow_execute_script === 'boolean'
|
|
54
|
+
? a.allow_execute_script
|
|
55
|
+
: base.automation.allow_execute_script,
|
|
56
|
+
},
|
|
50
57
|
ps_path: typeof r.ps_path === 'string' ? r.ps_path : null,
|
|
51
58
|
update_check: typeof r.update_check === 'boolean' ? r.update_check : base.update_check,
|
|
52
59
|
editor: typeof r.editor === 'string' && EDITOR_PINS.includes(r.editor)
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export const RECIPE_TOOLS = new Set([
|
|
2
|
+
'ps_template_apply',
|
|
3
|
+
'ps_batch',
|
|
4
|
+
'ps_sequence',
|
|
5
|
+
'ps_play_action',
|
|
6
|
+
]);
|
|
1
7
|
export const READ_ONLY_TOOLS = new Set([
|
|
2
8
|
'ps_sequence',
|
|
3
9
|
'ps_batch',
|
|
@@ -29,6 +35,7 @@ export const READ_ONLY_TOOLS = new Set([
|
|
|
29
35
|
'gimp_get_preview',
|
|
30
36
|
'gimp_get_histogram',
|
|
31
37
|
'gimp_compare',
|
|
38
|
+
'gimp_get_selection_preview',
|
|
32
39
|
]);
|
|
33
40
|
export const KEPT_WORK_TOOLS = new Set([
|
|
34
41
|
'ps_export',
|
|
@@ -109,14 +116,21 @@ export const MUTATING_TOOLS = new Set([
|
|
|
109
116
|
'gimp_crop_document',
|
|
110
117
|
'gimp_resize_image',
|
|
111
118
|
'gimp_transform_canvas',
|
|
112
|
-
'gimp_create_mask',
|
|
113
119
|
'gimp_checkpoint',
|
|
114
120
|
'gimp_layer',
|
|
115
121
|
'gimp_bake',
|
|
116
122
|
'gimp_create_document',
|
|
117
123
|
'gimp_place_image',
|
|
118
124
|
'gimp_canvas',
|
|
125
|
+
'gimp_text',
|
|
119
126
|
'gimp_convert_image_mode',
|
|
127
|
+
'gimp_select',
|
|
128
|
+
'gimp_modify_selection',
|
|
129
|
+
'gimp_layer_mask',
|
|
130
|
+
'gimp_transform_layer',
|
|
131
|
+
'gimp_match_layer',
|
|
132
|
+
'gimp_retouch',
|
|
133
|
+
'gimp_fill',
|
|
120
134
|
]);
|
|
121
135
|
export const RAW_DEVELOP_TOOL = 'ps_develop_raw';
|
|
122
136
|
export const CAMERA_RAW_TOOL = 'ps_apply_camera_raw';
|
package/dist/core/tool-groups.js
CHANGED
|
@@ -187,14 +187,22 @@ export const TOOL_GROUPS = {
|
|
|
187
187
|
gimp_add_adjustment: 'adjust',
|
|
188
188
|
gimp_filter: 'filter',
|
|
189
189
|
gimp_add_effect: 'filter',
|
|
190
|
-
gimp_create_mask: 'masks',
|
|
191
190
|
gimp_checkpoint: 'document',
|
|
192
191
|
gimp_layer: 'layers',
|
|
193
192
|
gimp_bake: 'layers',
|
|
193
|
+
gimp_transform_layer: 'layers',
|
|
194
194
|
gimp_create_document: 'document',
|
|
195
195
|
gimp_place_image: 'layers',
|
|
196
196
|
gimp_canvas: 'document',
|
|
197
|
+
gimp_text: 'layers',
|
|
197
198
|
gimp_convert_image_mode: 'document',
|
|
199
|
+
gimp_select: 'select',
|
|
200
|
+
gimp_modify_selection: 'select',
|
|
201
|
+
gimp_layer_mask: 'masks',
|
|
202
|
+
gimp_get_selection_preview: 'verify',
|
|
203
|
+
gimp_match_layer: 'layers',
|
|
204
|
+
gimp_retouch: 'layers',
|
|
205
|
+
gimp_fill: 'layers',
|
|
198
206
|
};
|
|
199
207
|
export function groupOf(toolName) {
|
|
200
208
|
const group = TOOL_GROUPS[toolName];
|
package/dist/core/tool-tiers.js
CHANGED
|
@@ -102,17 +102,25 @@ export const TOOL_TIERS = {
|
|
|
102
102
|
gimp_crop_document: 'community',
|
|
103
103
|
gimp_resize_image: 'community',
|
|
104
104
|
gimp_transform_canvas: 'community',
|
|
105
|
-
gimp_create_mask: 'community',
|
|
106
105
|
gimp_get_preview: 'community',
|
|
107
106
|
gimp_get_histogram: 'community',
|
|
108
107
|
gimp_compare: 'community',
|
|
109
108
|
gimp_checkpoint: 'community',
|
|
110
109
|
gimp_layer: 'community',
|
|
111
110
|
gimp_bake: 'community',
|
|
111
|
+
gimp_transform_layer: 'community',
|
|
112
112
|
gimp_create_document: 'community',
|
|
113
113
|
gimp_place_image: 'community',
|
|
114
114
|
gimp_canvas: 'community',
|
|
115
|
+
gimp_text: 'community',
|
|
115
116
|
gimp_convert_image_mode: 'community',
|
|
117
|
+
gimp_select: 'community',
|
|
118
|
+
gimp_modify_selection: 'community',
|
|
119
|
+
gimp_layer_mask: 'community',
|
|
120
|
+
gimp_get_selection_preview: 'community',
|
|
121
|
+
gimp_match_layer: 'dev',
|
|
122
|
+
gimp_retouch: 'dev',
|
|
123
|
+
gimp_fill: 'dev',
|
|
116
124
|
};
|
|
117
125
|
export function tierOf(toolName) {
|
|
118
126
|
const tier = TOOL_TIERS[toolName];
|
package/dist/delivery/client.js
CHANGED
|
@@ -157,6 +157,9 @@ export class DeliveryClient {
|
|
|
157
157
|
if (status === 404) {
|
|
158
158
|
return new DeliveryError('Requested module/version was not found.', 404, 'not_found');
|
|
159
159
|
}
|
|
160
|
+
if (status === 451) {
|
|
161
|
+
return new DeliveryError("Editmamei Pro downloads aren't available in your region.", 451, 'unavailable_in_region');
|
|
162
|
+
}
|
|
160
163
|
return new DeliveryError(`Delivery request failed (HTTP ${status}).`, status, 'server', retryAfterMs);
|
|
161
164
|
}
|
|
162
165
|
}
|
|
@@ -8,11 +8,16 @@ import { createGimpAdjustmentTools } from '../../tools/gimp-adjustment-tools.js'
|
|
|
8
8
|
import { createGimpEffectTools } from '../../tools/gimp-effect-tools.js';
|
|
9
9
|
import { createGimpFilterTools } from '../../tools/gimp-filter-tools.js';
|
|
10
10
|
import { createGimpGeometryTools } from '../../tools/gimp-geometry-tools.js';
|
|
11
|
-
import { createGimpMaskTools } from '../../tools/gimp-mask-tools.js';
|
|
12
11
|
import { createGimpVerifyTools } from '../../tools/gimp-verify-tools.js';
|
|
13
12
|
import { createGimpCheckpointTools } from '../../tools/gimp-checkpoint-tools.js';
|
|
14
13
|
import { createGimpLayerTools } from '../../tools/gimp-layer-tools.js';
|
|
14
|
+
import { createGimpTransformLayerTools } from '../../tools/gimp-transform-layer-tools.js';
|
|
15
15
|
import { createGimpComposeTools } from '../../tools/gimp-compose-tools.js';
|
|
16
|
+
import { createGimpTextTools } from '../../tools/gimp-text-tools.js';
|
|
17
|
+
import { createGimpSelectionTools } from '../../tools/gimp-selection-tools.js';
|
|
18
|
+
import { createGimpMatchTools } from '../../tools/gimp-match-tools.js';
|
|
19
|
+
import { createGimpRetouchTools } from '../../tools/gimp-retouch-tools.js';
|
|
20
|
+
import { createGimpFillTools } from '../../tools/gimp-fill-tools.js';
|
|
16
21
|
export const gimpFactories = [
|
|
17
22
|
createGimpCoreTools,
|
|
18
23
|
createGimpDocumentTools,
|
|
@@ -21,11 +26,16 @@ export const gimpFactories = [
|
|
|
21
26
|
createGimpEffectTools,
|
|
22
27
|
createGimpFilterTools,
|
|
23
28
|
createGimpGeometryTools,
|
|
24
|
-
createGimpMaskTools,
|
|
25
29
|
createGimpVerifyTools,
|
|
26
30
|
createGimpCheckpointTools,
|
|
27
31
|
createGimpLayerTools,
|
|
32
|
+
createGimpTransformLayerTools,
|
|
28
33
|
createGimpComposeTools,
|
|
34
|
+
createGimpTextTools,
|
|
35
|
+
createGimpSelectionTools,
|
|
36
|
+
createGimpMatchTools,
|
|
37
|
+
createGimpRetouchTools,
|
|
38
|
+
createGimpFillTools,
|
|
29
39
|
];
|
|
30
40
|
export const gimpModule = {
|
|
31
41
|
manifest: { id: 'gimp', name: 'Editmamei GIMP Tools', abi: KERNEL_ABI },
|
|
Binary file
|
|
@@ -23,8 +23,22 @@ function homedirPrefixes() {
|
|
|
23
23
|
return [...set];
|
|
24
24
|
}
|
|
25
25
|
const SINGLE_SEG_POSIX = /(^|\s)\/[^\s"'/]+(?=$|\s)/;
|
|
26
|
+
const WORD = String.raw `[^\s"'\\/:]+`;
|
|
27
|
+
const DIR = String.raw `${WORD}(?: ${WORD})*`;
|
|
28
|
+
const TILDE_WORD = String.raw `[^\s"'\\/]+`;
|
|
29
|
+
const TILDE_DIR = String.raw `${TILDE_WORD}(?: ${TILDE_WORD})*`;
|
|
30
|
+
const POSIX_WORD = String.raw `[^\s"'/]+`;
|
|
31
|
+
const POSIX_DIR = String.raw `${POSIX_WORD}(?: ${POSIX_WORD})*`;
|
|
32
|
+
const PATH_TOKEN = new RegExp([
|
|
33
|
+
String.raw `[A-Za-z]:[\\/](?:${DIR}[\\/])*[^\s"']*`,
|
|
34
|
+
String.raw `\\\\(?:${DIR}[\\/])*[^\s"']+`,
|
|
35
|
+
String.raw `file:\/\/[^\s"']*`,
|
|
36
|
+
String.raw `(?<![\w~])~(?:[\\/]${TILDE_DIR}(?=[\\/]))*[\\/]${TILDE_WORD}[\\/]?`,
|
|
37
|
+
String.raw `\/${POSIX_WORD}(?:\/${POSIX_DIR}(?=\/))*\/${POSIX_WORD}\/?`,
|
|
38
|
+
String.raw `(?:^|\s)\/[^\s"'/]+(?=$|\s)`,
|
|
39
|
+
].join('|'), 'gi');
|
|
26
40
|
function basenamePaths(s) {
|
|
27
|
-
return s.replace(
|
|
41
|
+
return s.replace(PATH_TOKEN, (m) => {
|
|
28
42
|
const single = SINGLE_SEG_POSIX.exec(m);
|
|
29
43
|
if (single)
|
|
30
44
|
return `${single[1]}/…`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { runScript } from '../utils/run-script.js';
|
|
2
2
|
import { validateArgs } from '../utils/validate.js';
|
|
3
3
|
import { toolErrorResult, runSnippetTool, applyToActiveLayerProp } from '../utils/tool-helpers.js';
|
|
4
|
+
import { looksLikePath, requireLocalPath } from '../utils/local-path.js';
|
|
4
5
|
import { getPendingRawDevelop } from '../core/raw-develop-state.js';
|
|
5
6
|
import { CAMERA_RAW_TOOL } from '../core/tool-activity.js';
|
|
6
7
|
const addAdjustmentLayerSchema = {
|
|
@@ -802,6 +803,11 @@ const FORWARDED_KEYS = new Set([
|
|
|
802
803
|
'pos_levels',
|
|
803
804
|
'thr_level',
|
|
804
805
|
]);
|
|
806
|
+
function lutNameOrPath(value) {
|
|
807
|
+
return typeof value === 'string' && looksLikePath(value)
|
|
808
|
+
? requireLocalPath('cl_lut_name', value)
|
|
809
|
+
: value;
|
|
810
|
+
}
|
|
805
811
|
async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
806
812
|
try {
|
|
807
813
|
const args = validateArgs(addAdjustmentLayerSchema, rawArgs);
|
|
@@ -816,6 +822,8 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
|
|
|
816
822
|
if (args[key] !== undefined)
|
|
817
823
|
params[key] = args[key];
|
|
818
824
|
}
|
|
825
|
+
if (typeof params.cl_lut_name === 'string')
|
|
826
|
+
params.cl_lut_name = lutNameOrPath(params.cl_lut_name);
|
|
819
827
|
if (type === 'color_lookup' && !params.cl_lut_name) {
|
|
820
828
|
return {
|
|
821
829
|
content: [
|
|
@@ -875,7 +883,7 @@ async function applyColorLookup(connection, snippetClient, rawArgs) {
|
|
|
875
883
|
snippet: 'applyColorLookup',
|
|
876
884
|
errorPrefix: 'Error applying color lookup',
|
|
877
885
|
params: (args) => ({
|
|
878
|
-
lutName: args.cl_lut_name,
|
|
886
|
+
lutName: lutNameOrPath(args.cl_lut_name),
|
|
879
887
|
applyToActiveLayer: args.apply_to_active_layer ?? false,
|
|
880
888
|
}),
|
|
881
889
|
successText: (result) => `Color Lookup applied (BAKE, experimental):\n${JSON.stringify(result, null, 2)}`,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
1
2
|
import { runScript } from '../utils/run-script.js';
|
|
2
3
|
import { validateArgs } from '../utils/validate.js';
|
|
3
|
-
import { OPEN_DOCUMENT_TIMEOUT_MS, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS, } from '../utils/operation-timeouts.js';
|
|
4
|
+
import { OPEN_DOCUMENT_TIMEOUT_MS, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS, SAVE_PSD_REPROBE_TIMEOUT_MS, getToolTimeoutMs, } from '../utils/operation-timeouts.js';
|
|
4
5
|
import { toolErrorResult, runSnippetTool } from '../utils/tool-helpers.js';
|
|
6
|
+
import { checkOutputPath, recordOutputPath, requireLocalPath } from '../utils/local-path.js';
|
|
5
7
|
import { purgeSceneChannels } from '../perception/region-precompute.js';
|
|
6
8
|
import { Logger } from '../utils/logger.js';
|
|
7
9
|
const logger = new Logger('document-tools');
|
|
@@ -76,7 +78,7 @@ const openDocumentSchema = {
|
|
|
76
78
|
properties: {
|
|
77
79
|
file_path: {
|
|
78
80
|
type: 'string',
|
|
79
|
-
description:
|
|
81
|
+
description: "Absolute path to the file to open, on this computer's own drives (network shares are refused).",
|
|
80
82
|
},
|
|
81
83
|
suppress_dialogs: {
|
|
82
84
|
type: 'boolean',
|
|
@@ -96,7 +98,14 @@ const savePsdSchema = {
|
|
|
96
98
|
properties: {
|
|
97
99
|
output_path: {
|
|
98
100
|
type: 'string',
|
|
99
|
-
description: 'Absolute output path including filename, e.g. E:\\Photos\\Edit\\shell_01.psd',
|
|
101
|
+
description: 'Absolute output path including filename, ending in .psd, e.g. E:\\Photos\\Edit\\shell_01.psd',
|
|
102
|
+
},
|
|
103
|
+
overwrite: {
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
description: 'Replace an existing file at output_path that this session did not write. Default false: such a ' +
|
|
106
|
+
'save is refused, so an original is never replaced by accident. Set true only when the user asked ' +
|
|
107
|
+
'to replace that file. A file this session already wrote can be saved over without it.',
|
|
108
|
+
default: false,
|
|
100
109
|
},
|
|
101
110
|
maximize_compatibility: {
|
|
102
111
|
type: 'boolean',
|
|
@@ -116,7 +125,14 @@ const exportJpegSchema = {
|
|
|
116
125
|
properties: {
|
|
117
126
|
output_path: {
|
|
118
127
|
type: 'string',
|
|
119
|
-
description: 'Absolute output path including filename.',
|
|
128
|
+
description: 'Absolute output path including filename, ending in .jpg.',
|
|
129
|
+
},
|
|
130
|
+
overwrite: {
|
|
131
|
+
type: 'boolean',
|
|
132
|
+
description: 'Replace an existing file at output_path that this session did not write. Default false: such a ' +
|
|
133
|
+
'save is refused, so an original is never replaced by accident. Set true only when the user asked ' +
|
|
134
|
+
'to replace that file. A file this session already wrote can be saved over without it.',
|
|
135
|
+
default: false,
|
|
120
136
|
},
|
|
121
137
|
quality: {
|
|
122
138
|
type: 'integer',
|
|
@@ -149,7 +165,14 @@ const exportPngSchema = {
|
|
|
149
165
|
properties: {
|
|
150
166
|
output_path: {
|
|
151
167
|
type: 'string',
|
|
152
|
-
description: 'Absolute output path including filename.',
|
|
168
|
+
description: 'Absolute output path including filename, ending in .png.',
|
|
169
|
+
},
|
|
170
|
+
overwrite: {
|
|
171
|
+
type: 'boolean',
|
|
172
|
+
description: 'Replace an existing file at output_path that this session did not write. Default false: such a ' +
|
|
173
|
+
'save is refused, so an original is never replaced by accident. Set true only when the user asked ' +
|
|
174
|
+
'to replace that file. A file this session already wrote can be saved over without it.',
|
|
175
|
+
default: false,
|
|
153
176
|
},
|
|
154
177
|
transparent_background: {
|
|
155
178
|
type: 'boolean',
|
|
@@ -327,6 +350,15 @@ export function createDocumentTools(connection, snippetClient) {
|
|
|
327
350
|
saved_to: { type: 'string' },
|
|
328
351
|
document_name: { type: 'string' },
|
|
329
352
|
layers: { type: 'number' },
|
|
353
|
+
reprobed: {
|
|
354
|
+
type: 'boolean',
|
|
355
|
+
description: 'Present when the save outran its time budget and a follow-up check confirmed the file was written. document_name, layers and context are absent then.',
|
|
356
|
+
},
|
|
357
|
+
bytes: {
|
|
358
|
+
type: 'number',
|
|
359
|
+
description: 'Size of the written file. Present only with reprobed.',
|
|
360
|
+
},
|
|
361
|
+
scene_channels_purged: { type: 'number' },
|
|
330
362
|
context: { type: 'object' },
|
|
331
363
|
},
|
|
332
364
|
required: ['success'],
|
|
@@ -531,7 +563,7 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
|
531
563
|
let filePath;
|
|
532
564
|
try {
|
|
533
565
|
const args = validateArgs(openDocumentSchema, rawArgs);
|
|
534
|
-
filePath = args.file_path;
|
|
566
|
+
filePath = requireLocalPath('file_path', args.file_path);
|
|
535
567
|
const suppressDialogs = args.suppress_dialogs;
|
|
536
568
|
const bitDepth = args.bit_depth;
|
|
537
569
|
const script = await snippetClient.build('openDocumentPipeline', {
|
|
@@ -579,10 +611,49 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
|
579
611
|
return toolErrorResult('Error opening document', error);
|
|
580
612
|
}
|
|
581
613
|
}
|
|
614
|
+
async function reprobeSavedPsd(connection, outputPath, startedAt) {
|
|
615
|
+
try {
|
|
616
|
+
await runScript(connection, 'return { ready: true };', SAVE_PSD_REPROBE_TIMEOUT_MS);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
let size;
|
|
622
|
+
let modifiedMs;
|
|
623
|
+
try {
|
|
624
|
+
const stats = statSync(outputPath);
|
|
625
|
+
if (!stats.isFile())
|
|
626
|
+
return null;
|
|
627
|
+
size = stats.size;
|
|
628
|
+
modifiedMs = stats.mtimeMs;
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
if (size === 0 || modifiedMs < startedAt - 2_000)
|
|
634
|
+
return null;
|
|
635
|
+
const payload = { success: true, saved_to: outputPath, bytes: size, reprobed: true };
|
|
636
|
+
return {
|
|
637
|
+
content: [
|
|
638
|
+
{
|
|
639
|
+
type: 'text',
|
|
640
|
+
text: `PSD saved (the save ran past the ${getToolTimeoutMs('ps_save_psd')}ms budget and was reported as a ` +
|
|
641
|
+
`timeout, but a post-timeout check confirmed the file was written):\n` +
|
|
642
|
+
JSON.stringify(payload, null, 2),
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
structuredContent: payload,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
582
648
|
async function savePsdPipeline(connection, snippetClient, rawArgs) {
|
|
583
649
|
let args;
|
|
650
|
+
let outputPath;
|
|
584
651
|
try {
|
|
585
652
|
args = validateArgs(savePsdSchema, rawArgs);
|
|
653
|
+
outputPath = checkOutputPath('output_path', args.output_path, {
|
|
654
|
+
extensions: ['.psd'],
|
|
655
|
+
overwrite: args.overwrite === true,
|
|
656
|
+
});
|
|
586
657
|
}
|
|
587
658
|
catch (error) {
|
|
588
659
|
return toolErrorResult('Error saving PSD', error);
|
|
@@ -597,19 +668,33 @@ async function savePsdPipeline(connection, snippetClient, rawArgs) {
|
|
|
597
668
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
598
669
|
}
|
|
599
670
|
}
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
snippet: 'savePsdAsCopy',
|
|
606
|
-
errorPrefix: 'Error saving PSD',
|
|
607
|
-
params: (args) => ({
|
|
608
|
-
outputPath: args.output_path,
|
|
671
|
+
const startedAt = Date.now();
|
|
672
|
+
let res;
|
|
673
|
+
try {
|
|
674
|
+
const script = await snippetClient.build('savePsdAsCopy', {
|
|
675
|
+
outputPath,
|
|
609
676
|
maximizeCompat: args.maximize_compatibility,
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
|
|
677
|
+
});
|
|
678
|
+
const result = await runScript(connection, script, getToolTimeoutMs('ps_save_psd'));
|
|
679
|
+
res = {
|
|
680
|
+
content: [{ type: 'text', text: `PSD saved:\n${JSON.stringify(result, null, 2)}` }],
|
|
681
|
+
structuredContent: result,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
686
|
+
if (isScriptTimeoutError(message)) {
|
|
687
|
+
res =
|
|
688
|
+
(await reprobeSavedPsd(connection, outputPath, startedAt)) ??
|
|
689
|
+
toolErrorResult('Error saving PSD', new Error(`${message} Photoshop may still be writing the file: check whether ${outputPath} ` +
|
|
690
|
+
'was just modified before saving again.'));
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
res = toolErrorResult('Error saving PSD', error);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (!res.isError)
|
|
697
|
+
recordOutputPath(outputPath);
|
|
613
698
|
if (!res.isError && typeof res.structuredContent === 'object' && res.structuredContent !== null) {
|
|
614
699
|
res.structuredContent.scene_channels_purged = purged;
|
|
615
700
|
}
|
|
@@ -618,7 +703,10 @@ async function savePsdPipeline(connection, snippetClient, rawArgs) {
|
|
|
618
703
|
async function exportJpegPipeline(connection, snippetClient, rawArgs) {
|
|
619
704
|
try {
|
|
620
705
|
const args = validateArgs(exportJpegSchema, rawArgs);
|
|
621
|
-
const outputPath = args.output_path
|
|
706
|
+
const outputPath = checkOutputPath('output_path', args.output_path, {
|
|
707
|
+
extensions: ['.jpg'],
|
|
708
|
+
overwrite: args.overwrite === true,
|
|
709
|
+
});
|
|
622
710
|
const qualityPct = args.quality;
|
|
623
711
|
const quality = Math.round((qualityPct / 100) * 12);
|
|
624
712
|
const longEdgePx = args.long_edge_px;
|
|
@@ -630,6 +718,7 @@ async function exportJpegPipeline(connection, snippetClient, rawArgs) {
|
|
|
630
718
|
const script = await snippetClient.build('exportJpegPipeline', params);
|
|
631
719
|
const result = (await runScript(connection, script));
|
|
632
720
|
const structured = { ...result, quality: qualityPct, quality_ps_scale: quality };
|
|
721
|
+
recordOutputPath(outputPath);
|
|
633
722
|
return {
|
|
634
723
|
content: [
|
|
635
724
|
{
|
|
@@ -645,7 +734,8 @@ async function exportJpegPipeline(connection, snippetClient, rawArgs) {
|
|
|
645
734
|
}
|
|
646
735
|
}
|
|
647
736
|
async function exportPngPipeline(connection, snippetClient, rawArgs) {
|
|
648
|
-
|
|
737
|
+
let outputPath;
|
|
738
|
+
const res = await runSnippetTool({
|
|
649
739
|
connection,
|
|
650
740
|
snippetClient,
|
|
651
741
|
rawArgs,
|
|
@@ -654,8 +744,12 @@ async function exportPngPipeline(connection, snippetClient, rawArgs) {
|
|
|
654
744
|
errorPrefix: 'Error exporting PNG',
|
|
655
745
|
params: (args) => {
|
|
656
746
|
const longEdgePx = args.long_edge_px;
|
|
747
|
+
outputPath = checkOutputPath('output_path', args.output_path, {
|
|
748
|
+
extensions: ['.png'],
|
|
749
|
+
overwrite: args.overwrite === true,
|
|
750
|
+
});
|
|
657
751
|
const params = {
|
|
658
|
-
outputPath
|
|
752
|
+
outputPath,
|
|
659
753
|
transparentBg: args.transparent_background,
|
|
660
754
|
compression: args.compression,
|
|
661
755
|
};
|
|
@@ -665,4 +759,7 @@ async function exportPngPipeline(connection, snippetClient, rawArgs) {
|
|
|
665
759
|
},
|
|
666
760
|
successText: (result) => `PNG exported:\n${JSON.stringify(result, null, 2)}`,
|
|
667
761
|
});
|
|
762
|
+
if (!res.isError && outputPath !== undefined)
|
|
763
|
+
recordOutputPath(outputPath);
|
|
764
|
+
return res;
|
|
668
765
|
}
|
|
@@ -3,6 +3,7 @@ import { validateArgs } from '../utils/validate.js';
|
|
|
3
3
|
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
4
4
|
import { resolveGatedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
5
5
|
import { toolErrorResult, runSnippetTool, applyToActiveLayerProp, asSmartFilterProp, unknownDiscriminator, } from '../utils/tool-helpers.js';
|
|
6
|
+
import { requireLocalPath } from '../utils/local-path.js';
|
|
6
7
|
import { LAYER_BLEND_MODES } from '../utils/blend-modes.js';
|
|
7
8
|
import { runSmartFilterOp } from './smart-object-tools.js';
|
|
8
9
|
const gaussianBlurSchema = {
|
|
@@ -1652,7 +1653,7 @@ async function applyDisplace(connection, snippetClient, rawArgs) {
|
|
|
1652
1653
|
snippet: 'applyDisplace',
|
|
1653
1654
|
errorPrefix: 'Error applying Displace',
|
|
1654
1655
|
params: (args) => ({
|
|
1655
|
-
mapPath: args.map_path,
|
|
1656
|
+
mapPath: requireLocalPath('map_path', args.map_path),
|
|
1656
1657
|
horizontalScale: args.horizontal_scale ?? 10,
|
|
1657
1658
|
verticalScale: args.vertical_scale ?? 10,
|
|
1658
1659
|
displacementMap: args.displacement_map ?? 'stretch_to_fit',
|