editmamei 0.22.4 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +8 -6
  2. package/dist/api/extendscript/_helpers.js +97 -13
  3. package/dist/api/photoshop-api.js +4 -3
  4. package/dist/api/snippet-client.js +38 -2
  5. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  6. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  7. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  8. package/dist/core/server.js +56 -180
  9. package/dist/delivery/client.js +50 -1
  10. package/dist/delivery/provision.js +18 -1
  11. package/dist/delivery/signing.js +38 -1
  12. package/dist/delivery/store.js +149 -16
  13. package/dist/detection/detect-active-doc.js +41 -10
  14. package/dist/detection/detection-client.js +3 -3
  15. package/dist/detection/face-detector.js +2 -2
  16. package/dist/detection/geometry.js +14 -0
  17. package/dist/detection/landmark-detection-client.js +3 -3
  18. package/dist/detection/mesh-face.js +2 -3
  19. package/dist/detection/object-detector.js +2 -2
  20. package/dist/detection/runtime.js +14 -3
  21. package/dist/detection/sam-segmenter.js +69 -26
  22. package/dist/kernel/kernel.js +5 -9
  23. package/dist/kernel/module-lifecycle.js +181 -0
  24. package/dist/license/entitlement.js +13 -3
  25. package/dist/license/store.js +8 -1
  26. package/dist/modules/ce/index.js +1 -1
  27. package/dist/perception/facets.js +19 -1
  28. package/dist/perception/grounding-anchors.js +2 -12
  29. package/dist/perception/grounding-locate.js +1 -9
  30. package/dist/perception/grounding-review-crop.js +4 -29
  31. package/dist/perception/overlay.js +62 -0
  32. package/dist/perception/pixel-identity.js +41 -0
  33. package/dist/perception/region-precompute.js +85 -17
  34. package/dist/perception/scene-model.js +93 -50
  35. package/dist/perception/select-recipes.js +107 -66
  36. package/dist/perception/sky-ground-flood.js +0 -5
  37. package/dist/perception/sky-mask-transfer.js +72 -13
  38. package/dist/platform/connection.js +42 -11
  39. package/dist/platform/executor-queue.js +68 -0
  40. package/dist/platform/macos-detector.js +2 -1
  41. package/dist/platform/macos-executor.js +18 -67
  42. package/dist/platform/run-child.js +6 -3
  43. package/dist/platform/windows-detector.js +14 -7
  44. package/dist/platform/windows-executor.js +9 -64
  45. package/dist/skills/editmamei-skill.zip +0 -0
  46. package/dist/tools/adjustment-tools.js +78 -130
  47. package/dist/tools/brush-tools.js +3 -14
  48. package/dist/tools/channel-compose-tools.js +4 -23
  49. package/dist/tools/detection-tools.js +59 -47
  50. package/dist/tools/document-tools.js +121 -131
  51. package/dist/tools/filter-tools.js +489 -730
  52. package/dist/tools/group-tools.js +90 -156
  53. package/dist/tools/guide-tools.js +52 -83
  54. package/dist/tools/history-tools.js +29 -63
  55. package/dist/tools/image-placement-tools.js +18 -37
  56. package/dist/tools/image-tools.js +23 -51
  57. package/dist/tools/layer-ordering-tools.js +2 -9
  58. package/dist/tools/layer-properties-tools.js +245 -384
  59. package/dist/tools/layer-tools.js +115 -181
  60. package/dist/tools/layer-transform-tools.js +41 -106
  61. package/dist/tools/metadata-tools.js +2 -9
  62. package/dist/tools/path-tools.js +3 -14
  63. package/dist/tools/portrait-tools.js +2 -5
  64. package/dist/tools/preview-tools.js +53 -65
  65. package/dist/tools/retouch-tools.js +32 -70
  66. package/dist/tools/scene-tools.js +94 -73
  67. package/dist/tools/selection-tools.js +306 -563
  68. package/dist/tools/shape-tools.js +18 -10
  69. package/dist/tools/text-tools.js +24 -57
  70. package/dist/tools/transform-canvas-tools.js +21 -54
  71. package/dist/tools/vector-mask-tools.js +2 -9
  72. package/dist/utils/jpeg-quality.js +1 -0
  73. package/dist/utils/logger.js +3 -0
  74. package/dist/utils/operation-timeouts.js +7 -0
  75. package/dist/utils/run-script.js +7 -2
  76. package/dist/utils/session-log.js +134 -12
  77. package/dist/utils/template-storage.js +5 -6
  78. package/dist/utils/tool-helpers.js +37 -0
  79. package/dist/version.js +1 -1
  80. package/package.json +1 -1
package/README.md CHANGED
@@ -76,11 +76,11 @@ These are the choices that shape the surface, and the reason an AI assistant can
76
76
 
77
77
  ## Tool surface
78
78
 
79
- **75 tools across 16 capability groups** (58 Community, 17 Pro). Every tool is namespaced `photoshop_*` and discoverable at runtime via `tools/list`. Community tools ship in both editions; Pro tools unlock with a license.
79
+ **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.
80
80
 
81
81
  | Group | Edition | Tools |
82
82
  | --- | --- | --- |
83
- | **Core** | Community | `ping` · `overview` · `list_capabilities` · `undo` · `redo` |
83
+ | **Core** | Community | `ping` · `overview` · `list_capabilities` · `undo` · `redo` · `report_problem` |
84
84
  | **Inspect** | Community | `inspect` · `get_preview` |
85
85
  | **Verify** | Community | `get_histogram` · `compare_regions` · `get_layer_bounds_diff` · `get_selection_preview` |
86
86
  | **Document & canvas** | Community | `create_document` · `open_document` · `close_document` · `save_psd` · `export` · `place_image` · `resize_image` · `crop_document` · `convert_image_mode` · `transform_canvas` · `guides` |
@@ -88,13 +88,15 @@ These are the choices that shape the surface, and the reason an AI assistant can
88
88
  | **Adjustments** | Community | `add_adjustment_layer` · `apply_adjustment` |
89
89
  | **Filters** | Community | `apply_filter` |
90
90
  | **Retouch** | Community | `retouch` |
91
- | **Layers** | Community | `create_layer` · `delete_layer` · `fill_layer` · `add_fill_layer` · `select_layer` · `move_layer_to_position` · `duplicate_layer` · `layer_via_copy` · `convert_to_smart_object` · `rasterize_layer` · `set_layer` · `merge` · `bake_layer` · `add_layer_style` · `transform_layer` · `create_group` · `move_layer_to_group` · `set_group_blend_mode` · `ungroup` · `delete_group` |
91
+ | **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` |
92
92
  | **Masks & paths** | Community | `layer_mask` · `create_clipping_mask` · `path` · `vector_mask` · `apply_image` · `calculations` |
93
93
  | **Type** | Community | `create_text_layer` · `set_text` |
94
- | **Perception** | Community | `detect` · `scene` · `select_by_reference` |
94
+ | **Perception** | Community | `detect` · `read_scene` · `select_by_reference` |
95
95
  | **AI selection** | Community | `select_subject` · `select_sky` |
96
- | **AI selection** | Pro | `select_subject_instance` |
97
- | **Layers (warp)** | Pro | `warp_layer` · `warp_layer_mesh` |
96
+ | **AI selection** | Pro | `select_subject_instance` · `select_object` |
97
+ | **Filters** | Pro | `apply_camera_raw` |
98
+ | **Layers (warp)** | Pro | `warp_layer` · `warp_layer_mesh` · `warp_layer_along` · `warp_layer_region` · `warp_layer_to` |
99
+ | **Perception** | Pro | `edit_object` · `add_text_to_object` · `resolve_placement` |
98
100
  | **Face mesh** | Pro | `detect_landmarks` · `select_face_feature` |
99
101
  | **Templates** | Pro | `template_create_evidence` · `template_save` · `template_list` · `template_apply` · `template_verify` · `template_recall` · `template_delete` |
100
102
  | **Automation** | Pro | `list_actions` · `play_action` · `execute_script` |
@@ -3,18 +3,6 @@ export const helperFunctions = `
3
3
  function cTID(s) { return app.charIDToTypeID(s); }
4
4
  function sTID(s) { return app.stringIDToTypeID(s); }
5
5
  `;
6
- export const bitsPerChannelHelper = `
7
- function getBitsPerChannelInt(doc) {
8
- try {
9
- var bpc = doc.bitsPerChannel;
10
- if (bpc === BitsPerChannelType.ONE) return 1;
11
- if (bpc === BitsPerChannelType.EIGHT) return 8;
12
- if (bpc === BitsPerChannelType.SIXTEEN) return 16;
13
- if (bpc === BitsPerChannelType.THIRTYTWO) return 32;
14
- } catch (e) {}
15
- return 0;
16
- }
17
- `;
18
6
  export const normNameHelper = `
19
7
  function normName(s) {
20
8
  if (s === null || s === undefined) return '';
@@ -205,7 +193,29 @@ export function duplicateForOp(opName, applyToActiveLayer) {
205
193
  var __opTargetIsCopy = true;
206
194
  `;
207
195
  }
196
+ export const countLayersRecursiveHelper = `
197
+ function __countLayersRecursive(layers) {
198
+ var total = 0;
199
+ var n = 0;
200
+ try { n = layers.length; } catch (eN) { n = 0; }
201
+ for (var i = 0; i < n; i++) {
202
+ var candidate = null;
203
+ try { candidate = layers[i]; } catch (eC) { continue; }
204
+ if (!candidate) continue;
205
+ total++;
206
+ var isGroup = false;
207
+ try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
208
+ if (isGroup) {
209
+ var childLayers = null;
210
+ try { childLayers = candidate.layers; } catch (eCl) { childLayers = null; }
211
+ if (childLayers) total += __countLayersRecursive(childLayers);
212
+ }
213
+ }
214
+ return total;
215
+ }
216
+ `;
208
217
  export const getContextInfo = `
218
+ ${countLayersRecursiveHelper}
209
219
  function getContextInfo() {
210
220
  // The whole body is wrapped in try/catch — a destructive op that
211
221
  // succeeded but then triggered "document closed" / "active layer gone"
@@ -228,6 +238,7 @@ function getContextInfo() {
228
238
  resolution: doc.resolution,
229
239
  colorMode: String(doc.mode),
230
240
  layerCount: doc.layers.length,
241
+ total_layer_count: __countLayersRecursive(doc.layers),
231
242
  hasSelection: (function () {
232
243
  // ExtendScript throws "No such element" when accessing doc.selection.bounds
233
244
  // with no active selection, and that error is NOT catchable when DoJavaScript
@@ -242,12 +253,35 @@ function getContextInfo() {
242
253
 
243
254
  if (doc.activeLayer) {
244
255
  var layer = doc.activeLayer;
256
+
257
+ // layer.visible on the ExtendScript DOM returns EFFECTIVE
258
+ // visibility (parent-chain AND own flag) NO MATTER which proxy
259
+ // resolves it -- re-reading through a fresh doc.layers walk does
260
+ // NOT recover the layer's own flag (verified live 2026-07: a
261
+ // visible child inside a hidden group reads visible:false from
262
+ // every DOM proxy, active-layer or walked). The own flag is only
263
+ // readable via Action Manager's Vsbl property, keyed by the
264
+ // layer's stable id (putIdentifier, not putName -- names aren't
265
+ // unique). Confirmed live on both an ArtLayer and a LayerSet.
266
+ // Falls back to the DOM (effective) value if the AM read throws,
267
+ // so a failure degrades to the old behavior instead of breaking
268
+ // the context block every tool returns.
269
+ var __effectiveVisible = layer.visible;
270
+ var __ownVisible = __effectiveVisible;
271
+ try {
272
+ var __visRef = new ActionReference();
273
+ __visRef.putProperty(app.charIDToTypeID('Prpr'), app.charIDToTypeID('Vsbl'));
274
+ __visRef.putIdentifier(app.charIDToTypeID('Lyr '), layer.id);
275
+ __ownVisible = app.executeActionGet(__visRef).getBoolean(app.charIDToTypeID('Vsbl'));
276
+ } catch (eOwn) {}
277
+
245
278
  context.activeLayer = {
246
279
  name: layer.name,
247
280
  kind: String(layer.kind),
248
281
  opacity: layer.opacity,
249
282
  blendMode: String(layer.blendMode),
250
- visible: layer.visible,
283
+ visible: __ownVisible,
284
+ effectively_visible: __effectiveVisible,
251
285
  locked: layer.allLocked,
252
286
  isBackground: layer.isBackgroundLayer
253
287
  };
@@ -302,3 +336,53 @@ function getMinimalContextInfo() {
302
336
  }
303
337
  }
304
338
  `;
339
+ export const parentPathHelper = `
340
+ function __parentPathOf(doc, layer) {
341
+ function __ppWalk(layers, trail) {
342
+ var n = 0;
343
+ try { n = layers.length; } catch (eN) {}
344
+ for (var i = 0; i < n; i++) {
345
+ var candidate = null;
346
+ try { candidate = layers[i]; } catch (eC) { continue; }
347
+ if (!candidate) continue;
348
+ if (candidate === layer) return trail;
349
+ var isGroup = false;
350
+ try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
351
+ if (isGroup) {
352
+ var cname = null;
353
+ try { cname = candidate.name; } catch (eNm) {}
354
+ var childLayers = null;
355
+ try { childLayers = candidate.layers; } catch (eCl) {}
356
+ if (childLayers) {
357
+ var found = __ppWalk(childLayers, trail.concat([cname]));
358
+ if (found !== null) return found;
359
+ }
360
+ }
361
+ }
362
+ return null;
363
+ }
364
+ return __ppWalk(doc.layers, []);
365
+ }
366
+ `;
367
+ export const hoistFromActiveGroupHelper = `
368
+ function __hoistFromActiveGroupIfNeeded(doc, preMkActive, newLayer, intoActiveGroup) {
369
+ if (intoActiveGroup) return false;
370
+ var preWasGroup = false;
371
+ try { preWasGroup = (preMkActive instanceof LayerSet); } catch (eG) {}
372
+ if (!preWasGroup) return false;
373
+ var landedInside = false;
374
+ try {
375
+ var n = preMkActive.layers.length;
376
+ for (var i = 0; i < n; i++) {
377
+ if (preMkActive.layers[i] === newLayer) { landedInside = true; break; }
378
+ }
379
+ } catch (eIn) {}
380
+ if (!landedInside) return false;
381
+ try {
382
+ newLayer.move(preMkActive, ElementPlacement.PLACEBEFORE);
383
+ try { doc.activeLayer = newLayer; } catch (eA) {}
384
+ return true;
385
+ } catch (eMove) {}
386
+ return false;
387
+ }
388
+ `;
@@ -7,11 +7,12 @@ export class PhotoshopAPIFactory {
7
7
  this.connection = connection;
8
8
  }
9
9
  async createAPI() {
10
- const info = this.connection.getPhotoshopInfo();
10
+ const info = await this.connection.ensureDetected();
11
11
  if (!info) {
12
- throw new Error('Photoshop info not available. Please detect Photoshop first.');
12
+ throw new Error('Photoshop info not available — the local install could not be detected. ' +
13
+ 'Check that Photoshop is installed (set PHOTOSHOP_PATH to override detection).');
13
14
  }
14
- this.logger.info(`Creating ExtendScript API for Photoshop version ${info.version}`);
15
+ this.logger.debug(`Creating ExtendScript API for Photoshop version ${info.version}`);
15
16
  return new ExtendScriptPhotoshopAPI(this.connection);
16
17
  }
17
18
  }
@@ -52,21 +52,49 @@ export class CompositeSnippetClient {
52
52
  return target.build(name, params);
53
53
  }
54
54
  }
55
+ const SNIPPET_CACHE_MAX_ENTRIES = 256;
55
56
  export class GoSnippetClient {
56
57
  binaryPath;
57
58
  timeoutMs;
58
59
  extraEnv;
60
+ cache = new Map();
61
+ cachedBinaryMtimeMs = null;
59
62
  constructor(opts = {}) {
60
63
  this.binaryPath = opts.binaryPath ?? resolveCoreBinaryPath();
61
64
  this.timeoutMs = opts.timeoutMs ?? 10_000;
62
- this.extraEnv = opts.env ?? {};
65
+ this.extraEnv = { ...(opts.env ?? {}) };
66
+ }
67
+ refreshCacheValidity() {
68
+ let mtimeMs;
69
+ try {
70
+ mtimeMs = statSync(this.binaryPath).mtimeMs;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ if (mtimeMs !== this.cachedBinaryMtimeMs) {
76
+ this.cache.clear();
77
+ this.cachedBinaryMtimeMs = mtimeMs;
78
+ }
79
+ return true;
63
80
  }
64
81
  async build(name, params = {}) {
65
82
  ensureExecutable(this.binaryPath);
83
+ const paramsJson = JSON.stringify(params);
84
+ const cacheUsable = this.refreshCacheValidity();
85
+ const cacheKey = name + '\u0000' + paramsJson;
86
+ if (cacheUsable) {
87
+ const cached = this.cache.get(cacheKey);
88
+ if (cached !== undefined) {
89
+ this.cache.delete(cacheKey);
90
+ this.cache.set(cacheKey, cached);
91
+ return cached;
92
+ }
93
+ }
66
94
  const env = Object.keys(this.extraEnv).length > 0 ? { ...process.env, ...this.extraEnv } : undefined;
67
95
  const result = await runChildWithTimeout(this.binaryPath, ['build', name], {
68
96
  timeout: this.timeoutMs,
69
- input: JSON.stringify(params),
97
+ input: paramsJson,
70
98
  diagLabel: `editmamei-core build ${name}`,
71
99
  env,
72
100
  });
@@ -74,6 +102,14 @@ export class GoSnippetClient {
74
102
  throw new Error(`editmamei-core build "${name}" failed (exit ${result.exitCode}): ` +
75
103
  `${result.stderr.trim() || '(no stderr)'}`);
76
104
  }
105
+ if (cacheUsable) {
106
+ this.cache.set(cacheKey, result.stdout);
107
+ if (this.cache.size > SNIPPET_CACHE_MAX_ENTRIES) {
108
+ const oldestKey = this.cache.keys().next().value;
109
+ if (oldestKey !== undefined)
110
+ this.cache.delete(oldestKey);
111
+ }
112
+ }
77
113
  return result.stdout;
78
114
  }
79
115
  }
Binary file
Binary file
@@ -13,30 +13,18 @@ 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 { join, dirname } from 'node:path';
17
- import { pathToFileURL } from 'node:url';
18
- import { GoSnippetClient, resolveProBinaryPath, coreBinaryName } from '../api/snippet-client.js';
16
+ import { join } from 'node:path';
17
+ import { GoSnippetClient, coreBinaryName } from '../api/snippet-client.js';
19
18
  import { isProEntitled } from '../license/entitlement.js';
20
19
  import { createPingLicenseRefresher } from '../license/ping-refresh.js';
21
- import { loadVerifiedModule, readInstalledModule, installedPath, PRO_SKU, } from '../delivery/store.js';
22
- import { provisionModules } from '../delivery/provision.js';
23
- import { readLicense } from '../license/store.js';
24
- import { existsSync } from 'node:fs';
25
20
  import { runScript } from '../utils/run-script.js';
26
21
  import { listTemplates } from '../utils/template-storage.js';
22
+ import { toolErrorResult } from '../utils/tool-helpers.js';
27
23
  import { Kernel } from '../kernel/kernel.js';
28
- import { HOST_MIN_ABI } from '../kernel/host-api.js';
29
24
  import { ceModule } from '../modules/ce/index.js';
30
25
  import { hostDetectionRuntime } from '../detection/runtime.js';
31
- export function classifyModuleOutcome(inputs) {
32
- if (inputs.proModuleLoaded && inputs.skipReason === null)
33
- return 'loaded';
34
- if (inputs.skipReason === 'corrupt')
35
- return 'skipped_corrupt';
36
- if (inputs.skipReason === 'incompatible')
37
- return 'skipped_incompatible';
38
- return inputs.entitled ? 'absent' : 'lapsed';
39
- }
26
+ import { ModuleLifecycle, classifyModuleOutcome } from '../kernel/module-lifecycle.js';
27
+ export { classifyModuleOutcome };
40
28
  let logScriptOnErrorWarned = false;
41
29
  function warnLogScriptOnErrorOnce(logger) {
42
30
  if (logScriptOnErrorWarned)
@@ -64,8 +52,10 @@ export class EditmameiServer {
64
52
  sessionLog;
65
53
  telemetry;
66
54
  kernel;
67
- proModule = null;
68
- moduleSkipReason = null;
55
+ moduleLifecycle;
56
+ get moduleSkipReason() {
57
+ return this.moduleLifecycle.moduleSkipReason;
58
+ }
69
59
  psVersion = null;
70
60
  updateInfo = null;
71
61
  snippetClient = new GoSnippetClient();
@@ -134,7 +124,7 @@ export class EditmameiServer {
134
124
  this.toolRegistry.register('ps_ping', {
135
125
  tool: {
136
126
  name: 'ps_ping',
137
- description: 'Test connection to Photoshop and report session-start discovery signals. Read-only and idempotent. Call this once at the start of a session to confirm liveness before invoking any other tool. **If the user has given you an open-ended editing task, ALSO call `ps_overview` after this** — it returns the workflow contract + capabilities map + verification primitives so you can plan well. Returns `version` (folds the deleted photoshop_get_version), `custom_action_sets` (count of Action Sets loaded in the Photoshop Actions palette — non-zero means recorded workflows exist; if action tools are available in this build they will appear in `tools/list`), `user_templates` (count of saved Editmamei templates), and `open_documents` (names of documents already open in Photoshop, so the LLM does not have to ask which doc to edit).',
127
+ description: 'Test connection to Photoshop and report session-start discovery signals. Read-only and idempotent. Call this once at the start of a session to confirm liveness before invoking any other tool. **If the user has given you an open-ended editing task, ALSO call `ps_overview` after this** — it returns the workflow contract + capabilities map + verification primitives so you can plan well. Returns `version` (folds the removed get-version tool), `custom_action_sets` (count of Action Sets loaded in the Photoshop Actions palette — non-zero means recorded workflows exist; if action tools are available in this build they will appear in `tools/list`), `user_templates` (count of saved Editmamei templates), and `open_documents` (names of documents already open in Photoshop, so the LLM does not have to ask which doc to edit).',
138
128
  inputSchema: {
139
129
  type: 'object',
140
130
  properties: {},
@@ -150,7 +140,7 @@ export class EditmameiServer {
150
140
  degraded: {
151
141
  type: 'array',
152
142
  items: { type: 'string' },
153
- description: 'List of optional discovery signals that could not be gathered (e.g. "pingState" if the in-PS state snippet failed, "templates" if ~/.editmamei/templates was unreadable). Empty when all signals were collected cleanly. Returned defaults for any degraded field cannot be trusted as ground truth.',
143
+ description: 'List of optional discovery signals that could not be gathered (e.g. "pingState" if the go-core snippet builder itself failed to produce the in-PS state script — a broken/missing local install, unrelated to whether Photoshop is reachable; liveness falls back to a cheap ping probe in that case — "templates" if ~/.editmamei/templates was unreadable). Empty when all signals were collected cleanly. Returned defaults for any degraded field cannot be trusted as ground truth.',
154
144
  },
155
145
  update_available: {
156
146
  type: ['object', 'null'],
@@ -204,162 +194,39 @@ export class EditmameiServer {
204
194
  },
205
195
  handler: async () => this.listCapabilities(),
206
196
  });
207
- this.proModule = this.resolveProModule();
197
+ this.moduleLifecycle = new ModuleLifecycle({
198
+ toolRegistry: this.toolRegistry,
199
+ logger: this.logger,
200
+ assertToolsClassified: () => this.assertToolsClassified(),
201
+ });
202
+ const proModule = this.moduleLifecycle.resolveProModule();
208
203
  this.kernel = new Kernel({
209
204
  registry: this.toolRegistry,
210
205
  connection: this.session.getConnection(),
211
206
  snippet: this.snippetClient,
212
207
  detection: hostDetectionRuntime(),
213
- resolveModuleSnippet: (manifest) => manifest.goCoreSnippets && manifest.goCoreSnippets.length > 0 && this.proModule
214
- ? new GoSnippetClient({ binaryPath: join(this.proModule.binDir, coreBinaryName()) })
208
+ resolveModuleSnippet: (manifest) => manifest.goCoreSnippets && manifest.goCoreSnippets.length > 0 && proModule
209
+ ? new GoSnippetClient({ binaryPath: join(proModule.binDir, coreBinaryName()) })
215
210
  : null,
216
211
  sessionId: this.session.getSessionId(),
217
212
  logger: this.logger,
218
213
  });
214
+ this.moduleLifecycle.setKernel(this.kernel);
219
215
  this.kernel.loadBuiltins([ceModule]);
220
216
  this.assertToolsClassified();
221
217
  this.logger.info(`Registered ${this.toolRegistry.count()} tools (edition: ${EDITION})`);
222
218
  }
223
- resolveProModule() {
224
- if (isProEntitled()) {
225
- const verified = loadVerifiedModule(PRO_SKU);
226
- if (verified) {
227
- const abi = readInstalledModule(PRO_SKU)?.abi ?? null;
228
- return {
229
- importer: () => import(pathToFileURL(verified.handlersPath).href),
230
- binDir: verified.binDir,
231
- abi,
232
- };
233
- }
234
- if (EDITION !== 'dev' && existsSync(installedPath(PRO_SKU))) {
235
- this.moduleSkipReason = 'corrupt';
236
- }
237
- }
238
- if (EDITION === 'dev') {
239
- return {
240
- importer: () => import('../modules/pro/index.js'),
241
- binDir: dirname(resolveProBinaryPath()),
242
- abi: null,
243
- };
244
- }
245
- return null;
246
- }
247
219
  async loadModules() {
248
- if (!this.proModule)
249
- return;
250
- if (this.proModule.abi !== null && this.proModule.abi < HOST_MIN_ABI) {
251
- this.logger.warn(`Pro module (abi ${this.proModule.abi}) is older than this host requires ` +
252
- `(min abi ${HOST_MIN_ABI}) — booting Community; will re-provision in the background.`);
253
- this.moduleSkipReason = 'incompatible';
254
- return;
255
- }
256
- const snapshot = this.toolRegistry.snapshot();
257
- try {
258
- await this.kernel.loadDownloaded(this.proModule.importer);
259
- this.assertToolsClassified();
260
- }
261
- catch (err) {
262
- const changed = this.toolRegistry.count() - snapshot.size;
263
- this.toolRegistry.restore(snapshot);
264
- this.logger.warn(`Pro module could not be loaded on this host — booting Community and rolling back ` +
265
- `${changed} module tool change(s); will re-provision in the background: ` +
266
- `${err instanceof Error ? err.message : String(err)}`);
267
- this.moduleSkipReason = 'incompatible';
268
- }
220
+ return this.moduleLifecycle.loadModules();
269
221
  }
270
222
  computeModuleStatus() {
271
- if (!readLicense())
272
- return null;
273
- const installed = readInstalledModule(PRO_SKU);
274
- return {
275
- module: PRO_SKU,
276
- outcome: classifyModuleOutcome({
277
- proModuleLoaded: this.proModule !== null,
278
- skipReason: this.moduleSkipReason,
279
- entitled: isProEntitled(),
280
- }),
281
- module_version: installed?.version ?? null,
282
- abi: installed?.abi ?? null,
283
- };
223
+ return this.moduleLifecycle.computeModuleStatus();
284
224
  }
285
225
  async reprovisionIfModuleSkipped(delivery = {}) {
286
- const reason = this.moduleSkipReason;
287
- if (reason === null)
288
- return;
289
- const license = readLicense();
290
- if (!license) {
291
- this.logger.warn('A Pro module was skipped but no cached license was found — staying Community. ' +
292
- 'Run `editmamei activate <key>` to restore Pro.');
293
- return;
294
- }
295
- try {
296
- const prov = await provisionModules(license.key, {
297
- force: reason === 'corrupt',
298
- config: delivery.config,
299
- fetchImpl: delivery.fetchImpl,
300
- signingKeys: delivery.signingKeys,
301
- sleep: delivery.sleep,
302
- });
303
- if (prov.installed.length > 0) {
304
- for (const m of prov.installed) {
305
- this.logger.info(`Pro module updated to v${m.version}, restart to load.`);
306
- }
307
- return;
308
- }
309
- if (prov.notConfigured) {
310
- this.logger.warn('Module delivery is not configured — staying Community.');
311
- return;
312
- }
313
- for (const e of prov.errors) {
314
- this.logger.warn(`Could not re-provision the ${e.sku} module (staying Community): ${e.message}`);
315
- }
316
- if (prov.errors.length === 0) {
317
- if (reason === 'incompatible') {
318
- this.logger.warn('The published Pro module does not yet support this host version — staying ' +
319
- 'Community. Update Editmamei when a compatible release ships; ' +
320
- '`editmamei report` files a diagnostic.');
321
- }
322
- else {
323
- this.logger.warn('Re-provision installed nothing for the corrupt Pro module — staying ' +
324
- 'Community. Try `editmamei repair`; `editmamei report` if it persists.');
325
- }
326
- }
327
- }
328
- catch (err) {
329
- this.logger.warn(`Background Pro-module re-provision failed (staying Community): ` +
330
- `${err instanceof Error ? err.message : String(err)}`);
331
- }
226
+ return this.moduleLifecycle.reprovisionIfModuleSkipped(delivery);
332
227
  }
333
228
  async ensureEntitledModuleFresh(delivery = {}) {
334
- if (this.moduleSkipReason !== null)
335
- return;
336
- if (!isProEntitled())
337
- return;
338
- const underTest = process.env.VITEST !== undefined || process.env.NODE_ENV === 'test';
339
- if (underTest && !delivery.fetchImpl)
340
- return;
341
- const license = readLicense();
342
- if (!license)
343
- return;
344
- try {
345
- const prov = await provisionModules(license.key, {
346
- config: delivery.config,
347
- fetchImpl: delivery.fetchImpl,
348
- signingKeys: delivery.signingKeys,
349
- sleep: delivery.sleep,
350
- });
351
- for (const m of prov.installed) {
352
- this.logger.info(`Pro module updated to v${m.version} — restart to load.`);
353
- }
354
- for (const e of prov.errors) {
355
- this.logger.warn(`Pro-module freshness check could not provision the ${e.sku} module ` +
356
- `(staying on the installed version): ${e.message}`);
357
- }
358
- }
359
- catch (err) {
360
- this.logger.warn(`Background Pro-module freshness check failed (staying on the installed version): ` +
361
- `${err instanceof Error ? err.message : String(err)}`);
362
- }
229
+ return this.moduleLifecycle.ensureEntitledModuleFresh(delivery);
363
230
  }
364
231
  assertToolsClassified() {
365
232
  for (const tool of this.toolRegistry.list()) {
@@ -420,29 +287,12 @@ export class EditmameiServer {
420
287
  }
421
288
  catch (error) {
422
289
  this.logger.error(`Tool execution failed: ${name}`, error);
423
- return {
424
- content: [
425
- {
426
- type: 'text',
427
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
428
- },
429
- ],
430
- isError: true,
431
- };
290
+ return toolErrorResult('Error', error);
432
291
  }
433
292
  }
434
293
  async pingPhotoshop() {
435
294
  this.refreshLicenseOnPing();
436
295
  const connection = this.session.getConnection();
437
- const isConnected = await connection.ping();
438
- if (!isConnected) {
439
- return {
440
- content: [
441
- { type: 'text', text: 'Failed to connect to Photoshop' + this.updateNote() },
442
- ],
443
- structuredContent: { connected: false, update_available: this.updateInfo },
444
- };
445
- }
446
296
  let version = 'Unknown';
447
297
  let actionSetsCount = 0;
448
298
  let openDocuments = [];
@@ -454,8 +304,37 @@ export class EditmameiServer {
454
304
  }
455
305
  catch {
456
306
  }
307
+ let pingStateSnippet = null;
457
308
  try {
458
- const state = (await runScript(connection, await this.snippetClient.build('pingState')));
309
+ pingStateSnippet = await this.snippetClient.build('pingState');
310
+ }
311
+ catch (err) {
312
+ this.logger.warn(`pingState snippet build failed (go-core binary missing/broken?): ${err instanceof Error ? err.message : String(err)}`);
313
+ const alive = await connection.ping().catch(() => false);
314
+ if (!alive) {
315
+ return {
316
+ content: [
317
+ { type: 'text', text: 'Failed to connect to Photoshop' + this.updateNote() },
318
+ ],
319
+ structuredContent: { connected: false, update_available: this.updateInfo },
320
+ };
321
+ }
322
+ degraded.push('pingState');
323
+ }
324
+ if (pingStateSnippet !== null) {
325
+ let state;
326
+ try {
327
+ state = (await runScript(connection, pingStateSnippet));
328
+ }
329
+ catch (err) {
330
+ this.logger.warn(`pingState snippet failed: ${err instanceof Error ? err.message : String(err)}`);
331
+ return {
332
+ content: [
333
+ { type: 'text', text: 'Failed to connect to Photoshop' + this.updateNote() },
334
+ ],
335
+ structuredContent: { connected: false, update_available: this.updateInfo },
336
+ };
337
+ }
459
338
  if (state.version)
460
339
  version = state.version;
461
340
  if (typeof state.action_sets_count === 'number')
@@ -463,10 +342,6 @@ export class EditmameiServer {
463
342
  if (Array.isArray(state.open_documents))
464
343
  openDocuments = state.open_documents;
465
344
  }
466
- catch (err) {
467
- this.logger.warn(`pingState snippet failed: ${err instanceof Error ? err.message : String(err)}`);
468
- degraded.push('pingState');
469
- }
470
345
  let userTemplates = 0;
471
346
  try {
472
347
  const templates = await listTemplates();
@@ -527,6 +402,7 @@ export class EditmameiServer {
527
402
  async stop() {
528
403
  await this.telemetry.shutdown();
529
404
  await this.session.disconnect();
405
+ await this.sessionLog.close();
530
406
  this.logger.info('MCP Server stopped');
531
407
  }
532
408
  }