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.
Files changed (44) hide show
  1. package/README.md +9 -9
  2. package/dist/api/extendscript/_helpers.js +70 -2
  3. package/dist/api/photoshop-api.js +17 -2
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/activate.js +1 -1
  8. package/dist/cli/config.js +1 -1
  9. package/dist/cli/deactivate.js +1 -1
  10. package/dist/core/raw-develop-state.js +13 -0
  11. package/dist/core/server.js +147 -18
  12. package/dist/core/tool-groups.js +18 -21
  13. package/dist/core/tool-tiers.js +7 -10
  14. package/dist/detection/runtime.js +2 -2
  15. package/dist/modules/ce/index.js +2 -0
  16. package/dist/perception/region-scorer.js +1 -1
  17. package/dist/platform/connection.js +15 -0
  18. package/dist/skills/editmamei-skill.zip +0 -0
  19. package/dist/telemetry/client.js +15 -2
  20. package/dist/telemetry/sanitize.js +1 -0
  21. package/dist/tools/adjustment-tools.js +21 -3
  22. package/dist/tools/detection-tools.js +1 -1
  23. package/dist/tools/document-tools.js +5 -2
  24. package/dist/tools/filter-tools.js +118 -15
  25. package/dist/tools/group-tools.js +146 -146
  26. package/dist/tools/history-tools.js +1 -1
  27. package/dist/tools/inspect-tools.js +22 -3
  28. package/dist/tools/layer-tools.js +4 -27
  29. package/dist/tools/overview-tools.js +37 -8
  30. package/dist/tools/preview-tools.js +3 -2
  31. package/dist/tools/scene-tools.js +1 -1
  32. package/dist/tools/selection-tools.js +91 -18
  33. package/dist/tools/sky-tools.js +126 -0
  34. package/dist/tools/smart-object-tools.js +163 -0
  35. package/dist/tools/text-tools.js +62 -41
  36. package/dist/update/check.js +65 -6
  37. package/dist/update/session-fixes.js +33 -0
  38. package/dist/utils/jsx.js +2 -1
  39. package/dist/utils/operation-timeouts.js +2 -0
  40. package/dist/utils/session-log.js +46 -6
  41. package/dist/utils/temp.js +1 -1
  42. package/dist/utils/tool-helpers.js +23 -0
  43. package/dist/version.js +1 -1
  44. 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 20+ (for the npm path), and an MCP-compatible AI client (Claude Desktop, Cursor, Claude Code).
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
- **87 tools across 16 capability groups** (64 Community, 23 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.
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 | `apply_filter` |
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` · `create_group` · `move_layer_to_group` · `set_group_blend_mode` · `ungroup` · `delete_group` |
91
- | **Masks & paths** | Community | `layer_mask` · `create_clipping_mask` · `path` · `vector_mask` · `apply_image` · `calculations` |
92
- | **Type** | Community | `create_text_layer` · `set_text` |
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` · `warp_layer_mesh` · `warp_layer_along` · `warp_layer_region` · `warp_layer_to` |
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 20+ 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.
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. T05 P1-4 in the
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
@@ -33,7 +33,7 @@ export async function runActivate(opts = {}) {
33
33
  catch (e) {
34
34
  if (e instanceof PolarLicenseError) {
35
35
  err(`Activation failed: ${e.message}\n`);
36
- throw new Error(e.code);
36
+ throw new Error(e.code, { cause: e });
37
37
  }
38
38
  throw e;
39
39
  }
@@ -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);
@@ -12,7 +12,7 @@ export async function runDeactivate(opts = {}) {
12
12
  catch (e) {
13
13
  if (e instanceof PolarLicenseError) {
14
14
  err(`Deactivate failed: ${e.message}\n`);
15
- throw new Error(e.code);
15
+ throw new Error(e.code, { cause: e });
16
16
  }
17
17
  throw e;
18
18
  }
@@ -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
+ }
@@ -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
- void checkForUpdate().then((info) => {
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 errorClass = classifyError(entry.error);
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: entry.success,
113
+ success: telemetrySuccess,
99
114
  duration_ms: entry.duration_ms,
100
115
  error_class: errorClass,
101
116
  });
102
- if (!entry.success && entry.error) {
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. When present, TELL THE USER: relay current → latest and how_to_update verbatim. { current, latest, channel (npm/mcpb/dev), how_to_update }. Anonymous npm-registry check at boot; opt out with `editmamei config set update_check false`.',
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') + this.updateNote(),
409
+ text: (reason ?? 'Photoshop did not respond') + update.note,
322
410
  },
323
411
  ],
324
- structuredContent: { connected: false, update_available: this.updateInfo },
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
- { type: 'text', text: 'Photoshop did not respond' + this.updateNote() },
339
- ],
340
- structuredContent: { connected: false, update_available: this.updateInfo },
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
- this.updateNote(),
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
- updateNote() {
488
+ async buildUpdateNotice() {
388
489
  const u = this.updateInfo;
389
490
  if (!u)
390
- return '';
391
- return ` Update available: v${u.current} → v${u.latest}. ${u.how_to_update}`;
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();
@@ -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 & sky selection (Pro).',
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/patch/move and brush strokes.',
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 work paths.',
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 (Pro)',
70
- purpose: 'Face-landmark perception and mesh-aimed feature selection / contour strokes (Pro).',
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: 'Play recorded Actions and the ExtendScript escape hatch (Pro).',
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
- ps_apply_filter: 'filter',
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
- ps_create_group: 'layers',
141
- ps_move_layer_to_group: 'layers',
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
- ps_create_clipping_mask: 'masks',
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
- ps_shape: 'layers',
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];