editmamei 1.0.3 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/api/extendscript/_helpers.js +70 -2
- package/dist/api/photoshop-api.js +17 -2
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/activate.js +1 -1
- package/dist/cli/config.js +1 -1
- package/dist/cli/deactivate.js +1 -1
- package/dist/core/raw-develop-state.js +13 -0
- package/dist/core/server.js +147 -18
- package/dist/core/tool-groups.js +18 -21
- package/dist/core/tool-tiers.js +7 -10
- package/dist/detection/runtime.js +2 -2
- package/dist/modules/ce/index.js +2 -0
- package/dist/perception/region-scorer.js +1 -1
- package/dist/platform/connection.js +15 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/client.js +15 -2
- package/dist/telemetry/sanitize.js +1 -0
- package/dist/tools/adjustment-tools.js +21 -3
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/document-tools.js +5 -2
- package/dist/tools/filter-tools.js +118 -15
- package/dist/tools/group-tools.js +146 -146
- package/dist/tools/history-tools.js +1 -1
- package/dist/tools/inspect-tools.js +22 -3
- package/dist/tools/layer-tools.js +4 -27
- package/dist/tools/overview-tools.js +37 -8
- package/dist/tools/preview-tools.js +3 -2
- package/dist/tools/scene-tools.js +1 -1
- package/dist/tools/selection-tools.js +91 -18
- package/dist/tools/sky-tools.js +126 -0
- package/dist/tools/smart-object-tools.js +163 -0
- package/dist/tools/text-tools.js +62 -41
- package/dist/update/check.js +65 -6
- package/dist/update/session-fixes.js +33 -0
- package/dist/utils/jsx.js +2 -1
- package/dist/utils/operation-timeouts.js +2 -0
- package/dist/utils/session-log.js +46 -6
- package/dist/utils/temp.js +1 -1
- package/dist/utils/tool-helpers.js +23 -0
- package/dist/version.js +1 -1
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Then restart your AI client and ask it: _"Is Photoshop connected?"_
|
|
|
47
47
|
|
|
48
48
|
Prefer not to use a terminal? Claude Desktop users can install the one-click [`.mcpb` extension](https://github.com/editmamei/editmamei/releases/latest/download/editmamei.mcpb) directly (no system Node required, Claude Desktop ships its own runtime). Full walkthrough in the [install guide](docs/installation.md).
|
|
49
49
|
|
|
50
|
-
**Requirements:** Adobe Photoshop 2026 (v27.x), Windows 10/11 or macOS 12+, Node.js
|
|
50
|
+
**Requirements:** Adobe Photoshop 2026 (v27.x), Windows 10/11 or macOS 12+, Node.js 22+ (for the npm path), and an MCP-compatible AI client (Claude Desktop, Cursor, Claude Code).
|
|
51
51
|
|
|
52
52
|
## A session, end to end
|
|
53
53
|
|
|
@@ -75,7 +75,7 @@ These are the choices that shape the surface, and the reason an AI assistant can
|
|
|
75
75
|
|
|
76
76
|
## Tool surface
|
|
77
77
|
|
|
78
|
-
**
|
|
78
|
+
**79 tools across 16 capability groups** (59 Community, 20 Pro). Every tool is namespaced `ps_*` and discoverable at runtime via `tools/list`. Community tools ship in both editions; Pro tools unlock with a license.
|
|
79
79
|
|
|
80
80
|
| Group | Edition | Tools |
|
|
81
81
|
| --- | --- | --- |
|
|
@@ -85,20 +85,20 @@ These are the choices that shape the surface, and the reason an AI assistant can
|
|
|
85
85
|
| **Document & canvas** | Community | `create_document` · `open_document` · `close_document` · `save_psd` · `export` · `place_image` · `resize_image` · `crop_document` · `convert_image_mode` · `transform_canvas` · `guides` |
|
|
86
86
|
| **Selection** | Community | `select` · `modify_selection` · `selection_channel` |
|
|
87
87
|
| **Adjustments** | Community | `add_adjustment_layer` · `apply_adjustment` |
|
|
88
|
-
| **Filters** | Community | `
|
|
88
|
+
| **Filters** | Community | `filter` |
|
|
89
89
|
| **Retouch** | Community | `retouch` |
|
|
90
|
-
| **Layers** | Community | `create_layer` · `delete_layer` · `fill_layer` · `add_fill_layer` · `select_layer` · `move_layer_to_position` · `duplicate_layer` · `copy_to_new_layer` · `convert_to_smart_object` · `rasterize_layer` · `set_layer` · `merge` · `bake_layer` · `add_layer_style` · `transform_layer` · `shape` · `
|
|
91
|
-
| **Masks & paths** | Community | `layer_mask` · `
|
|
92
|
-
| **Type** | Community | `
|
|
90
|
+
| **Layers** | Community | `create_layer` · `delete_layer` · `fill_layer` · `add_fill_layer` · `select_layer` · `move_layer_to_position` · `duplicate_layer` · `copy_to_new_layer` · `convert_to_smart_object` · `rasterize_layer` · `set_layer` · `merge` · `bake_layer` · `add_layer_style` · `transform_layer` · `shape` · `group` |
|
|
91
|
+
| **Masks & paths** | Community | `layer_mask` · `clipping_mask` · `path` · `vector_mask` · `apply_image` · `calculations` |
|
|
92
|
+
| **Type** | Community | `text` |
|
|
93
93
|
| **Perception** | Community | `detect` · `read_scene` · `select_by_reference` |
|
|
94
94
|
| **AI selection** | Community | `select_subject` · `select_sky` |
|
|
95
95
|
| **AI selection** | Pro | `select_subject_instance` · `select_object` |
|
|
96
96
|
| **Filters** | Pro | `apply_camera_raw` |
|
|
97
|
-
| **Layers (warp)** | Pro | `warp_layer`
|
|
97
|
+
| **Layers (warp)** | Pro | `warp_layer` |
|
|
98
98
|
| **Perception** | Pro | `edit_object` · `add_text_to_object` · `resolve_placement` |
|
|
99
99
|
| **Face mesh** | Pro | `detect_landmarks` · `select_face_feature` |
|
|
100
100
|
| **Templates** | Pro | `template_create_evidence` · `template_save` · `template_list` · `template_apply` · `template_verify` · `template_recall` · `template_delete` |
|
|
101
|
-
| **Automation** | Pro | `list_actions` · `play_action` · `execute_script` |
|
|
101
|
+
| **Automation** | Pro | `list_actions` · `play_action` · `execute_script` · `batch` |
|
|
102
102
|
|
|
103
103
|
## Editions
|
|
104
104
|
|
|
@@ -119,7 +119,7 @@ npm run build
|
|
|
119
119
|
npm test
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
You need Node.js
|
|
122
|
+
You need Node.js 22+ and a Go toolchain (the build compiles the `editmamei-core` binary that generates Photoshop scripts). The build warns instead of failing when Go is missing, so you can still run the test suite; set `EDITMAMEI_CORE_BIN` to a prebuilt binary if you would rather not install Go.
|
|
123
123
|
|
|
124
124
|
The test suite runs without Photoshop. It verifies the ExtendScript Editmamei generates, never that Photoshop accepted it, so live verification against a real Photoshop is a separate step.
|
|
125
125
|
|
|
@@ -18,6 +18,75 @@ function normName(s) {
|
|
|
18
18
|
return result.replace(/\\s+/g, ' ').replace(/^\\s+|\\s+$/g, '').toLowerCase();
|
|
19
19
|
}
|
|
20
20
|
`;
|
|
21
|
+
export const notFoundMessageHelper = `
|
|
22
|
+
function __notFoundMessage(label, requested, groupsOnly) {
|
|
23
|
+
var kept = [];
|
|
24
|
+
var total = 0;
|
|
25
|
+
var walkBroke = false;
|
|
26
|
+
function __nfEsc(s) {
|
|
27
|
+
var out = '';
|
|
28
|
+
for (var c = 0; c < s.length; c++) {
|
|
29
|
+
var code = s.charCodeAt(c);
|
|
30
|
+
// Control characters and backslash only — see the doc comment above.
|
|
31
|
+
if (code < 32 || code === 127 || code === 92) {
|
|
32
|
+
var hex = code.toString(16);
|
|
33
|
+
while (hex.length < 4) hex = '0' + hex;
|
|
34
|
+
out += '\\\\u' + hex;
|
|
35
|
+
} else {
|
|
36
|
+
out += s.charAt(c);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function consider(layer) {
|
|
42
|
+
var nm = '';
|
|
43
|
+
try { nm = String(layer.name); } catch (eN) { walkBroke = true; return; }
|
|
44
|
+
total++;
|
|
45
|
+
if (kept.length >= 8) return;
|
|
46
|
+
nm = __nfEsc(nm);
|
|
47
|
+
if (nm.length > 40) {
|
|
48
|
+
nm = nm.substring(0, 40);
|
|
49
|
+
var cut = nm.lastIndexOf('\\\\u');
|
|
50
|
+
if (cut > 34) nm = nm.substring(0, cut);
|
|
51
|
+
// Non-ASCII is no longer escaped, so the cut counts raw UTF-16 units and
|
|
52
|
+
// can land between the halves of a surrogate pair. Drop a trailing high
|
|
53
|
+
// surrogate rather than emit a lone one.
|
|
54
|
+
var lastCode = nm.length > 0 ? nm.charCodeAt(nm.length - 1) : 0;
|
|
55
|
+
if (lastCode >= 0xD800 && lastCode <= 0xDBFF) nm = nm.substring(0, nm.length - 1);
|
|
56
|
+
nm = nm + '...';
|
|
57
|
+
}
|
|
58
|
+
kept.push(nm);
|
|
59
|
+
}
|
|
60
|
+
function walk(layers, depth) {
|
|
61
|
+
var n = 0;
|
|
62
|
+
try { n = layers.length; } catch (eL) { walkBroke = true; return; }
|
|
63
|
+
for (var i = 0; i < n; i++) {
|
|
64
|
+
var l = null;
|
|
65
|
+
try { l = layers[i]; } catch (eI) { walkBroke = true; continue; }
|
|
66
|
+
if (!l) continue;
|
|
67
|
+
var isGroup = false;
|
|
68
|
+
try { isGroup = (l instanceof LayerSet); } catch (eG) {}
|
|
69
|
+
if (isGroup || !groupsOnly) consider(l);
|
|
70
|
+
if (isGroup && depth < 32) {
|
|
71
|
+
try { walk(l.layers, depth + 1); } catch (eD) { walkBroke = true; }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
try { walk(app.activeDocument.layers, 0); } catch (eW) { walkBroke = true; }
|
|
76
|
+
if (walkBroke && total === 0) {
|
|
77
|
+
return label + ' not found: ' + __nfEsc(String(requested));
|
|
78
|
+
}
|
|
79
|
+
var have;
|
|
80
|
+
if (total === 0) {
|
|
81
|
+
have = groupsOnly ? '(no groups)' : '(none)';
|
|
82
|
+
} else {
|
|
83
|
+
have = kept.join(', ');
|
|
84
|
+
if (total > kept.length) have += ' (+' + (total - kept.length) + ' more)';
|
|
85
|
+
if (walkBroke) have += ' (list may be incomplete)';
|
|
86
|
+
}
|
|
87
|
+
return label + ' not found: ' + __nfEsc(String(requested)) + '. Have: ' + have;
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
21
90
|
export const selectionTypeHelpers = `
|
|
22
91
|
function mapSelType(s) {
|
|
23
92
|
if (s === 'add' || s === 'extend') return SelectionType.EXTEND;
|
|
@@ -222,8 +291,7 @@ function getContextInfo() {
|
|
|
222
291
|
// during the context probe should NOT make the op look failed to the
|
|
223
292
|
// caller. On any unexpected throw we return a degraded context with
|
|
224
293
|
// an error_reading field so the caller can see something happened
|
|
225
|
-
// but the structured result still arrives.
|
|
226
|
-
// launch-readiness review.
|
|
294
|
+
// but the structured result still arrives.
|
|
227
295
|
try {
|
|
228
296
|
var context = {
|
|
229
297
|
hasDocument: app.documents.length > 0
|
|
@@ -33,7 +33,7 @@ class ExtendScriptPhotoshopAPI {
|
|
|
33
33
|
'This usually means PS is in a stuck/modal state (a leaked preview duplicate ' +
|
|
34
34
|
'or a pending dialog from a prior timeout), or there is no active document. ' +
|
|
35
35
|
'Check that a document is open and try once more; if it persists, dismiss any ' +
|
|
36
|
-
'open Photoshop dialog.');
|
|
36
|
+
'open Photoshop dialog.', { cause: err });
|
|
37
37
|
}
|
|
38
38
|
throw err instanceof Error ? err : new Error(msg);
|
|
39
39
|
}
|
|
@@ -55,6 +55,21 @@ class ExtendScriptPhotoshopAPI {
|
|
|
55
55
|
// numbers, booleans, null/undefined, arrays, and plain objects. Cycles
|
|
56
56
|
// and very deep nesting are capped via __depth to avoid stack overflow
|
|
57
57
|
// on pathological inputs.
|
|
58
|
+
//
|
|
59
|
+
// Everything outside printable ASCII is escaped as a JSON u-escape, not
|
|
60
|
+
// just the control range. The Windows cscript stdout transport is
|
|
61
|
+
// codepage-bound and flattens raw non-ASCII to '?' (measured live,
|
|
62
|
+
// PS 27.2.0), so a layer Photoshop named itself in a non-English UI —
|
|
63
|
+
// 'Farbfuellung 1' spelled with the u-umlaut — used to reach the caller as
|
|
64
|
+
// 'Farbf?llung 1'. Naming that layer back at us then missed, because '?'
|
|
65
|
+
// is not what the layer is called. The escape is valid JSON, survives the
|
|
66
|
+
// transport, and JSON.parse restores the exact character, so the round trip
|
|
67
|
+
// is lossless rather than merely legible. Every outcome the script itself
|
|
68
|
+
// produces routes through here, errors included, so this covers messages as
|
|
69
|
+
// well as values. It does NOT cover the Windows shim's own COM failures
|
|
70
|
+
// (windows-runner.ts echoes Err.Description straight to stdout), nor raw
|
|
71
|
+
// non-ASCII sitting in a snippet's own source text — that arrives by a
|
|
72
|
+
// different path and is not addressed here.
|
|
58
73
|
function __mcpJsonEncode(v, __depth) {
|
|
59
74
|
if (__depth === undefined) __depth = 0;
|
|
60
75
|
if (__depth > 32) return '"<<truncated:max-depth>>"';
|
|
@@ -72,7 +87,7 @@ class ExtendScriptPhotoshopAPI {
|
|
|
72
87
|
else if (c === '\\t') s += '\\\\t';
|
|
73
88
|
else if (c === '\\b') s += '\\\\b';
|
|
74
89
|
else if (c === '\\f') s += '\\\\f';
|
|
75
|
-
else if (cc < 32) {
|
|
90
|
+
else if (cc < 32 || cc > 126) {
|
|
76
91
|
var hex = cc.toString(16);
|
|
77
92
|
while (hex.length < 4) hex = '0' + hex;
|
|
78
93
|
s += '\\\\u' + hex;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/activate.js
CHANGED
package/dist/cli/config.js
CHANGED
|
@@ -98,7 +98,7 @@ export function runConfig(args, io = {}) {
|
|
|
98
98
|
}
|
|
99
99
|
catch (e) {
|
|
100
100
|
err(`Invalid value for ${key}: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
101
|
-
throw new Error('config set: bad value');
|
|
101
|
+
throw new Error('config set: bad value', { cause: e });
|
|
102
102
|
}
|
|
103
103
|
const { settings } = loadSettings(io);
|
|
104
104
|
spec.set(settings, coerced);
|
package/dist/cli/deactivate.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let pending = null;
|
|
2
|
+
export function markRawOpened(documentName, filePath) {
|
|
3
|
+
pending = { documentName, filePath };
|
|
4
|
+
}
|
|
5
|
+
export function clearPendingRawDevelop() {
|
|
6
|
+
pending = null;
|
|
7
|
+
}
|
|
8
|
+
export function getPendingRawDevelop() {
|
|
9
|
+
return pending;
|
|
10
|
+
}
|
|
11
|
+
export function __clearRawDevelopState() {
|
|
12
|
+
pending = null;
|
|
13
|
+
}
|
package/dist/core/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import { loadSettings, applyTelemetryEnvOverrides, applyUpdateCheckEnvOverride,
|
|
|
13
13
|
import { TelemetryClient } from '../telemetry/client.js';
|
|
14
14
|
import { resolveInstallChannel } from '../install-channel.js';
|
|
15
15
|
import { checkForUpdate, shouldCheckForUpdate } from '../update/check.js';
|
|
16
|
+
import { previousSessionFailureCounts, relevantFixes } from '../update/session-fixes.js';
|
|
16
17
|
import { join } from 'node:path';
|
|
17
18
|
import { GoSnippetClient, coreBinaryName } from '../api/snippet-client.js';
|
|
18
19
|
import { isProEntitled } from '../license/entitlement.js';
|
|
@@ -21,6 +22,7 @@ import { runScript } from '../utils/run-script.js';
|
|
|
21
22
|
import { listTemplates } from '../utils/template-storage.js';
|
|
22
23
|
import { toolErrorResult } from '../utils/tool-helpers.js';
|
|
23
24
|
import { Kernel } from '../kernel/kernel.js';
|
|
25
|
+
import { markRawOpened, clearPendingRawDevelop } from './raw-develop-state.js';
|
|
24
26
|
import { ceModule } from '../modules/ce/index.js';
|
|
25
27
|
import { hostDetectionRuntime } from '../detection/runtime.js';
|
|
26
28
|
import { ModuleLifecycle, classifyModuleOutcome } from '../kernel/module-lifecycle.js';
|
|
@@ -45,6 +47,7 @@ const FIRST_RUN_DISCLOSURE = 'First run: Editmamei collects anonymous, content-f
|
|
|
45
47
|
'It never sends image content, file paths, or personal data. Opt out anytime: ' +
|
|
46
48
|
'`editmamei config set telemetry.usage false` (or edit ~/.editmamei/settings.json). ' +
|
|
47
49
|
'Opt in to sanitized diagnostics: `editmamei config set telemetry.diagnostics true`.';
|
|
50
|
+
export const BACKGROUND_VERSION_PROBE_TIMEOUT_MS = 3_000;
|
|
48
51
|
export class EditmameiServer {
|
|
49
52
|
server;
|
|
50
53
|
logger;
|
|
@@ -58,7 +61,11 @@ export class EditmameiServer {
|
|
|
58
61
|
return this.moduleLifecycle.moduleSkipReason;
|
|
59
62
|
}
|
|
60
63
|
psVersion = null;
|
|
64
|
+
lastPingReachedPs = null;
|
|
65
|
+
liveVersionResolutionAttempted = false;
|
|
61
66
|
updateInfo = null;
|
|
67
|
+
updateCheck = null;
|
|
68
|
+
updateNoticeShown = false;
|
|
62
69
|
snippetClient = new GoSnippetClient();
|
|
63
70
|
refreshLicenseOnPing = createPingLicenseRefresher();
|
|
64
71
|
constructor() {
|
|
@@ -78,13 +85,18 @@ export class EditmameiServer {
|
|
|
78
85
|
if (created)
|
|
79
86
|
this.logger.info(FIRST_RUN_DISCLOSURE);
|
|
80
87
|
if (shouldCheckForUpdate(effectiveSettings.update_check)) {
|
|
81
|
-
|
|
88
|
+
this.updateCheck = checkForUpdate().then((info) => {
|
|
82
89
|
this.updateInfo = info;
|
|
83
90
|
});
|
|
84
91
|
}
|
|
85
92
|
warnLogScriptOnErrorOnce(this.logger);
|
|
86
93
|
this.toolRegistry = new ToolRegistry({
|
|
87
94
|
onCall: (entry) => {
|
|
95
|
+
if (entry.success && entry.tool !== 'ps_ping' && this.psVersion === null) {
|
|
96
|
+
if (this.session.getConnection().hasReachedPhotoshop()) {
|
|
97
|
+
this.resolveLiveVersionInBackground();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
88
100
|
void this.sessionLog.append({
|
|
89
101
|
tool: entry.tool,
|
|
90
102
|
args: entry.args,
|
|
@@ -92,18 +104,24 @@ export class EditmameiServer {
|
|
|
92
104
|
duration_ms: entry.duration_ms,
|
|
93
105
|
...(entry.error ? { error: entry.error } : {}),
|
|
94
106
|
}, entry.result);
|
|
95
|
-
const
|
|
107
|
+
const telemetrySuccess = entry.tool === 'ps_ping' && this.lastPingReachedPs === false ? false : entry.success;
|
|
108
|
+
const isPingDowngrade = entry.tool === 'ps_ping' && !telemetrySuccess && entry.error === undefined;
|
|
109
|
+
const errorClass = classifyError(entry.error) ??
|
|
110
|
+
(telemetrySuccess ? null : isPingDowngrade ? 'ps_not_running' : 'other');
|
|
96
111
|
this.telemetry.recordCall({
|
|
97
112
|
tool: entry.tool,
|
|
98
|
-
success:
|
|
113
|
+
success: telemetrySuccess,
|
|
99
114
|
duration_ms: entry.duration_ms,
|
|
100
115
|
error_class: errorClass,
|
|
101
116
|
});
|
|
102
|
-
if (!
|
|
117
|
+
if (!telemetrySuccess) {
|
|
103
118
|
this.telemetry.recordDiagnostic({
|
|
104
119
|
tool: entry.tool,
|
|
105
120
|
error_class: errorClass ?? 'other',
|
|
106
|
-
error_message: entry.error
|
|
121
|
+
error_message: entry.error ||
|
|
122
|
+
(isPingDowngrade
|
|
123
|
+
? 'ps_ping did not reach Photoshop'
|
|
124
|
+
: 'tool reported failure with no message'),
|
|
107
125
|
});
|
|
108
126
|
}
|
|
109
127
|
},
|
|
@@ -145,14 +163,19 @@ export class EditmameiServer {
|
|
|
145
163
|
},
|
|
146
164
|
update_available: {
|
|
147
165
|
type: ['object', 'null'],
|
|
148
|
-
description: 'Set when a newer Editmamei version is published, else null
|
|
166
|
+
description: 'Set when a newer Editmamei version is published, else null: { current, latest, channel (npm/mcpb/dev), how_to_update, fixed_tools (tools whose recorded failures the newer version fixes) }. The relay instruction rides the ping TEXT on the first ping — see notify_user. Anonymous npm-registry check at boot; opt out with `editmamei config set update_check false`.',
|
|
149
167
|
properties: {
|
|
150
168
|
current: { type: 'string' },
|
|
151
169
|
latest: { type: 'string' },
|
|
152
170
|
channel: { type: 'string' },
|
|
153
171
|
how_to_update: { type: 'string' },
|
|
172
|
+
fixed_tools: { type: 'array', items: { type: 'string' } },
|
|
154
173
|
},
|
|
155
174
|
},
|
|
175
|
+
notify_user: {
|
|
176
|
+
type: 'boolean',
|
|
177
|
+
description: 'True on the one ping whose text carries the update notice — relay that notice to the user before continuing. False on later pings and when no update is available.',
|
|
178
|
+
},
|
|
156
179
|
},
|
|
157
180
|
},
|
|
158
181
|
annotations: {
|
|
@@ -284,6 +307,7 @@ export class EditmameiServer {
|
|
|
284
307
|
try {
|
|
285
308
|
const result = await this.toolRegistry.execute(name, args);
|
|
286
309
|
this.session.updateActivity();
|
|
310
|
+
this.trackRawDevelopState(name, result);
|
|
287
311
|
return result;
|
|
288
312
|
}
|
|
289
313
|
catch (error) {
|
|
@@ -291,13 +315,59 @@ export class EditmameiServer {
|
|
|
291
315
|
return toolErrorResult('Error', error);
|
|
292
316
|
}
|
|
293
317
|
}
|
|
318
|
+
trackRawDevelopState(name, result) {
|
|
319
|
+
if (result.isError)
|
|
320
|
+
return;
|
|
321
|
+
if (name === 'ps_open_document') {
|
|
322
|
+
const sc = result.structuredContent;
|
|
323
|
+
if (sc?.is_raw_source === true && this.toolRegistry.get('ps_apply_camera_raw')) {
|
|
324
|
+
const opened = sc;
|
|
325
|
+
markRawOpened(String(opened.document_name ?? ''), String(opened.file_path ?? ''));
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
clearPendingRawDevelop();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if (name === 'ps_apply_camera_raw' || name === 'ps_close_document') {
|
|
332
|
+
clearPendingRawDevelop();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
resolveLiveVersionInBackground() {
|
|
336
|
+
if (this.liveVersionResolutionAttempted || this.psVersion !== null)
|
|
337
|
+
return;
|
|
338
|
+
void (async () => {
|
|
339
|
+
try {
|
|
340
|
+
const connection = this.session.getConnection();
|
|
341
|
+
const snippet = await this.snippetClient.build('pingState');
|
|
342
|
+
if (!(await connection.isCurrentlyRunning()))
|
|
343
|
+
return;
|
|
344
|
+
if (this.liveVersionResolutionAttempted || this.psVersion !== null)
|
|
345
|
+
return;
|
|
346
|
+
this.liveVersionResolutionAttempted = true;
|
|
347
|
+
const state = (await runScript(connection, snippet, BACKGROUND_VERSION_PROBE_TIMEOUT_MS));
|
|
348
|
+
if (state.version && state.version !== 'Unknown' && this.psVersion === null) {
|
|
349
|
+
this.psVersion = state.version;
|
|
350
|
+
void this.sessionLog.setPsVersion(state.version);
|
|
351
|
+
this.telemetry.onPsVersionResolved();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
this.logger.debug(`background ps_version resolution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
356
|
+
}
|
|
357
|
+
})();
|
|
358
|
+
}
|
|
294
359
|
async pingPhotoshop() {
|
|
360
|
+
this.lastPingReachedPs = null;
|
|
295
361
|
this.refreshLicenseOnPing();
|
|
362
|
+
if (this.updateCheck)
|
|
363
|
+
await this.updateCheck;
|
|
364
|
+
const update = await this.buildUpdateNotice();
|
|
296
365
|
const connection = this.session.getConnection();
|
|
297
366
|
let version = 'Unknown';
|
|
298
367
|
let actionSetsCount = 0;
|
|
299
368
|
let openDocuments = [];
|
|
300
369
|
const degraded = [];
|
|
370
|
+
let versionIsLive = false;
|
|
301
371
|
try {
|
|
302
372
|
const detected = await connection.getVersion();
|
|
303
373
|
if (detected)
|
|
@@ -305,6 +375,23 @@ export class EditmameiServer {
|
|
|
305
375
|
}
|
|
306
376
|
catch {
|
|
307
377
|
}
|
|
378
|
+
if (connection.getPhotoshopInfo() !== null && !(await connection.isCurrentlyRunning())) {
|
|
379
|
+
this.lastPingReachedPs = false;
|
|
380
|
+
return {
|
|
381
|
+
content: [
|
|
382
|
+
{
|
|
383
|
+
type: 'text',
|
|
384
|
+
text: 'Photoshop does not appear to be running. Start Photoshop, then call ps_ping again.' +
|
|
385
|
+
update.note,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
structuredContent: {
|
|
389
|
+
connected: false,
|
|
390
|
+
update_available: this.updateInfo,
|
|
391
|
+
notify_user: update.notify,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
308
395
|
let pingStateSnippet = null;
|
|
309
396
|
try {
|
|
310
397
|
pingStateSnippet = await this.snippetClient.build('pingState');
|
|
@@ -314,14 +401,19 @@ export class EditmameiServer {
|
|
|
314
401
|
const alive = await connection.ping().catch(() => false);
|
|
315
402
|
if (!alive) {
|
|
316
403
|
const reason = unsupportedHostReason();
|
|
404
|
+
this.lastPingReachedPs = false;
|
|
317
405
|
return {
|
|
318
406
|
content: [
|
|
319
407
|
{
|
|
320
408
|
type: 'text',
|
|
321
|
-
text: (reason ?? 'Photoshop did not respond') +
|
|
409
|
+
text: (reason ?? 'Photoshop did not respond') + update.note,
|
|
322
410
|
},
|
|
323
411
|
],
|
|
324
|
-
structuredContent: {
|
|
412
|
+
structuredContent: {
|
|
413
|
+
connected: false,
|
|
414
|
+
update_available: this.updateInfo,
|
|
415
|
+
notify_user: update.notify,
|
|
416
|
+
},
|
|
325
417
|
};
|
|
326
418
|
}
|
|
327
419
|
degraded.push('pingState');
|
|
@@ -333,15 +425,20 @@ export class EditmameiServer {
|
|
|
333
425
|
}
|
|
334
426
|
catch (err) {
|
|
335
427
|
this.logger.warn(`pingState snippet failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
428
|
+
this.lastPingReachedPs = false;
|
|
336
429
|
return {
|
|
337
|
-
content: [
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
430
|
+
content: [{ type: 'text', text: 'Photoshop did not respond' + update.note }],
|
|
431
|
+
structuredContent: {
|
|
432
|
+
connected: false,
|
|
433
|
+
update_available: this.updateInfo,
|
|
434
|
+
notify_user: update.notify,
|
|
435
|
+
},
|
|
341
436
|
};
|
|
342
437
|
}
|
|
343
|
-
if (state.version)
|
|
438
|
+
if (state.version) {
|
|
344
439
|
version = state.version;
|
|
440
|
+
versionIsLive = true;
|
|
441
|
+
}
|
|
345
442
|
if (typeof state.action_sets_count === 'number')
|
|
346
443
|
actionSetsCount = state.action_sets_count;
|
|
347
444
|
if (Array.isArray(state.open_documents))
|
|
@@ -357,10 +454,13 @@ export class EditmameiServer {
|
|
|
357
454
|
degraded.push('templates');
|
|
358
455
|
}
|
|
359
456
|
if (version !== 'Unknown') {
|
|
360
|
-
this.psVersion = version;
|
|
361
457
|
void this.sessionLog.setPsVersion(version);
|
|
458
|
+
}
|
|
459
|
+
if (versionIsLive && version !== 'Unknown') {
|
|
460
|
+
this.psVersion = version;
|
|
362
461
|
this.telemetry.onPsVersionResolved();
|
|
363
462
|
}
|
|
463
|
+
this.lastPingReachedPs = true;
|
|
364
464
|
const degradedNote = degraded.length ? ` (degraded: ${degraded.join(', ')})` : '';
|
|
365
465
|
return {
|
|
366
466
|
content: [
|
|
@@ -370,7 +470,7 @@ export class EditmameiServer {
|
|
|
370
470
|
`${actionSetsCount} custom action set(s), ${userTemplates} saved template(s), ` +
|
|
371
471
|
`${openDocuments.length} open document(s)${openDocuments.length ? ': ' + openDocuments.join(', ') : ''}` +
|
|
372
472
|
`${degradedNote}.` +
|
|
373
|
-
|
|
473
|
+
update.note,
|
|
374
474
|
},
|
|
375
475
|
],
|
|
376
476
|
structuredContent: {
|
|
@@ -381,14 +481,43 @@ export class EditmameiServer {
|
|
|
381
481
|
open_documents: openDocuments,
|
|
382
482
|
degraded,
|
|
383
483
|
update_available: this.updateInfo,
|
|
484
|
+
notify_user: update.notify,
|
|
384
485
|
},
|
|
385
486
|
};
|
|
386
487
|
}
|
|
387
|
-
|
|
488
|
+
async buildUpdateNotice() {
|
|
388
489
|
const u = this.updateInfo;
|
|
389
490
|
if (!u)
|
|
390
|
-
return '';
|
|
391
|
-
|
|
491
|
+
return { note: '', notify: false };
|
|
492
|
+
if (this.updateNoticeShown) {
|
|
493
|
+
return {
|
|
494
|
+
note: ` Update available: v${u.current} → v${u.latest}. ${u.how_to_update}`,
|
|
495
|
+
notify: false,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
this.updateNoticeShown = true;
|
|
499
|
+
let fixNote = '';
|
|
500
|
+
if (u.fixed_tools.length > 0) {
|
|
501
|
+
try {
|
|
502
|
+
const counts = await previousSessionFailureCounts(this.session.getSessionId(), {
|
|
503
|
+
dir: this.sessionLog.directory,
|
|
504
|
+
});
|
|
505
|
+
const hits = relevantFixes(counts, u.fixed_tools);
|
|
506
|
+
if (hits.length > 0) {
|
|
507
|
+
const named = hits
|
|
508
|
+
.map((h) => `${h.tool} (failed ${h.failures} time${h.failures === 1 ? '' : 's'} last session)`)
|
|
509
|
+
.join(', ');
|
|
510
|
+
fixNote = ` v${u.latest} fixes known failures in ${named}.`;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
note: ` IMPORTANT — tell the user before continuing: Editmamei v${u.latest} is available` +
|
|
518
|
+
` (this install runs v${u.current}).${fixNote} To update: ${u.how_to_update}`,
|
|
519
|
+
notify: true,
|
|
520
|
+
};
|
|
392
521
|
}
|
|
393
522
|
async start() {
|
|
394
523
|
await this.loadModules();
|
package/dist/core/tool-groups.js
CHANGED
|
@@ -7,12 +7,12 @@ export const GROUPS = {
|
|
|
7
7
|
inspect: {
|
|
8
8
|
id: 'inspect',
|
|
9
9
|
label: 'Inspect',
|
|
10
|
-
purpose: 'Read document/layer/selection/history state and render previews.',
|
|
10
|
+
purpose: 'Read document/layer/selection/history/smart-object state and render previews.',
|
|
11
11
|
},
|
|
12
12
|
verify: {
|
|
13
13
|
id: 'verify',
|
|
14
14
|
label: 'Verify',
|
|
15
|
-
purpose: 'Measurement primitives — histogram, region compare, bounds diff — to check work instead of eyeballing.',
|
|
15
|
+
purpose: 'Measurement primitives — histogram, region compare, bounds diff, selection preview — to check work instead of eyeballing.',
|
|
16
16
|
},
|
|
17
17
|
document: {
|
|
18
18
|
id: 'document',
|
|
@@ -22,12 +22,12 @@ export const GROUPS = {
|
|
|
22
22
|
select: {
|
|
23
23
|
id: 'select',
|
|
24
24
|
label: 'Selection',
|
|
25
|
-
purpose: 'Manual selections, selection edits, and alpha-channel save/load.',
|
|
25
|
+
purpose: 'Manual selections, selection edits, and alpha-channel save/load/duplicate/delete.',
|
|
26
26
|
},
|
|
27
27
|
select_ai: {
|
|
28
28
|
id: 'select_ai',
|
|
29
29
|
label: 'AI selection',
|
|
30
|
-
purpose: 'Sensei/CV-backed subject
|
|
30
|
+
purpose: 'Sensei/CV-backed selection: select_subject and select_sky (community), plus subject-instance selection and named-object selection (Pro).',
|
|
31
31
|
},
|
|
32
32
|
adjust: {
|
|
33
33
|
id: 'adjust',
|
|
@@ -37,12 +37,12 @@ export const GROUPS = {
|
|
|
37
37
|
filter: {
|
|
38
38
|
id: 'filter',
|
|
39
39
|
label: 'Filters',
|
|
40
|
-
purpose: 'Blur/sharpen/noise/stylize filters (auto-duplicate-first by default).',
|
|
40
|
+
purpose: 'Blur/sharpen/noise/stylize filters (auto-duplicate-first by default) and the Camera Raw develop surface, plus managing the re-editable Smart Filter stack on a Smart Object.',
|
|
41
41
|
},
|
|
42
42
|
retouch: {
|
|
43
43
|
id: 'retouch',
|
|
44
44
|
label: 'Retouch',
|
|
45
|
-
purpose: 'Content-aware fill
|
|
45
|
+
purpose: 'Content-aware fill, patch, and move.',
|
|
46
46
|
},
|
|
47
47
|
layers: {
|
|
48
48
|
id: 'layers',
|
|
@@ -52,7 +52,7 @@ export const GROUPS = {
|
|
|
52
52
|
masks: {
|
|
53
53
|
id: 'masks',
|
|
54
54
|
label: 'Masks & paths',
|
|
55
|
-
purpose: 'Layer masks, clipping masks, vector masks, and
|
|
55
|
+
purpose: 'Layer masks, clipping masks, vector masks, work paths, and channel compositing (apply image / calculations).',
|
|
56
56
|
},
|
|
57
57
|
type: {
|
|
58
58
|
id: 'type',
|
|
@@ -66,8 +66,8 @@ export const GROUPS = {
|
|
|
66
66
|
},
|
|
67
67
|
face: {
|
|
68
68
|
id: 'face',
|
|
69
|
-
label: 'Face mesh
|
|
70
|
-
purpose: 'Face-landmark perception and mesh-aimed feature selection / contour strokes
|
|
69
|
+
label: 'Face mesh',
|
|
70
|
+
purpose: 'Face-landmark perception and mesh-aimed feature selection / contour strokes. Pro in shipped builds.',
|
|
71
71
|
},
|
|
72
72
|
templates: {
|
|
73
73
|
id: 'templates',
|
|
@@ -77,7 +77,7 @@ export const GROUPS = {
|
|
|
77
77
|
automation: {
|
|
78
78
|
id: 'automation',
|
|
79
79
|
label: 'Automation (Pro)',
|
|
80
|
-
purpose: '
|
|
80
|
+
purpose: 'Batch files, play recorded Actions, and the ExtendScript escape hatch (Pro).',
|
|
81
81
|
},
|
|
82
82
|
};
|
|
83
83
|
export const TOOL_GROUPS = {
|
|
@@ -111,9 +111,11 @@ export const TOOL_GROUPS = {
|
|
|
111
111
|
ps_select_sky: 'select_ai',
|
|
112
112
|
ps_select_subject_instance: 'select_ai',
|
|
113
113
|
ps_select_object: 'select_ai',
|
|
114
|
+
ps_select_focus_area: 'select_ai',
|
|
115
|
+
ps_replace_sky: 'select_ai',
|
|
114
116
|
ps_add_adjustment_layer: 'adjust',
|
|
115
117
|
ps_apply_adjustment: 'adjust',
|
|
116
|
-
|
|
118
|
+
ps_filter: 'filter',
|
|
117
119
|
ps_apply_camera_raw: 'filter',
|
|
118
120
|
ps_retouch: 'retouch',
|
|
119
121
|
ps_apply_brush_stroke: 'retouch',
|
|
@@ -137,21 +139,15 @@ export const TOOL_GROUPS = {
|
|
|
137
139
|
ps_warp_layer_along: 'layers',
|
|
138
140
|
ps_warp_layer_region: 'layers',
|
|
139
141
|
ps_warp_layer_to: 'layers',
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
ps_set_group_blend_mode: 'layers',
|
|
143
|
-
ps_ungroup: 'layers',
|
|
144
|
-
ps_delete_group: 'layers',
|
|
142
|
+
ps_group: 'layers',
|
|
143
|
+
ps_shape: 'layers',
|
|
145
144
|
ps_layer_mask: 'masks',
|
|
146
|
-
|
|
147
|
-
ps_release_clipping_mask: 'masks',
|
|
145
|
+
ps_clipping_mask: 'masks',
|
|
148
146
|
ps_path: 'masks',
|
|
149
147
|
ps_vector_mask: 'masks',
|
|
150
148
|
ps_apply_image: 'masks',
|
|
151
149
|
ps_calculations: 'masks',
|
|
152
|
-
|
|
153
|
-
ps_create_text_layer: 'type',
|
|
154
|
-
ps_set_text: 'type',
|
|
150
|
+
ps_text: 'type',
|
|
155
151
|
ps_detect: 'perception',
|
|
156
152
|
ps_read_scene: 'perception',
|
|
157
153
|
ps_select_by_reference: 'perception',
|
|
@@ -172,6 +168,7 @@ export const TOOL_GROUPS = {
|
|
|
172
168
|
ps_list_actions: 'automation',
|
|
173
169
|
ps_play_action: 'automation',
|
|
174
170
|
ps_execute_script: 'automation',
|
|
171
|
+
ps_batch: 'automation',
|
|
175
172
|
};
|
|
176
173
|
export function groupOf(toolName) {
|
|
177
174
|
const group = TOOL_GROUPS[toolName];
|