editmamei 0.23.0 → 1.0.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 (90) hide show
  1. package/NOTICES.md +6 -30
  2. package/README.md +47 -13
  3. package/dist/api/extendscript/_helpers.js +1 -101
  4. package/dist/api/photoshop-api.js +23 -16
  5. package/dist/api/snippet-client.js +38 -2
  6. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  7. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  8. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  9. package/dist/core/server.js +60 -184
  10. package/dist/core/session.js +9 -10
  11. package/dist/core/tool-registry.js +4 -4
  12. package/dist/delivery/client.js +50 -1
  13. package/dist/delivery/provision.js +18 -1
  14. package/dist/delivery/signing.js +38 -1
  15. package/dist/delivery/store.js +157 -16
  16. package/dist/detection/detect-active-doc.js +42 -11
  17. package/dist/detection/detection-client.js +3 -3
  18. package/dist/detection/face-detector.js +2 -2
  19. package/dist/detection/geometry.js +14 -0
  20. package/dist/detection/landmark-detection-client.js +3 -3
  21. package/dist/detection/mesh-face.js +2 -3
  22. package/dist/detection/object-detector.js +2 -2
  23. package/dist/detection/runtime.js +14 -3
  24. package/dist/detection/sam-segmenter.js +69 -26
  25. package/dist/kernel/kernel.js +5 -9
  26. package/dist/kernel/module-lifecycle.js +187 -0
  27. package/dist/license/entitlement.js +13 -3
  28. package/dist/license/store.js +8 -1
  29. package/dist/modules/ce/index.js +1 -1
  30. package/dist/perception/facets.js +19 -1
  31. package/dist/perception/grounding-anchors.js +2 -12
  32. package/dist/perception/grounding-locate.js +1 -9
  33. package/dist/perception/grounding-review-crop.js +4 -29
  34. package/dist/perception/overlay.js +62 -0
  35. package/dist/perception/pixel-identity.js +41 -0
  36. package/dist/perception/region-precompute.js +96 -20
  37. package/dist/perception/scene-model.js +93 -50
  38. package/dist/perception/select-recipes.js +109 -68
  39. package/dist/perception/sky-ground-flood.js +0 -5
  40. package/dist/perception/sky-mask-transfer.js +73 -14
  41. package/dist/platform/connection.js +60 -43
  42. package/dist/platform/host-platform.js +16 -0
  43. package/dist/platform/macos-detector.js +41 -70
  44. package/dist/platform/macos-runner.js +94 -0
  45. package/dist/platform/script-queue.js +68 -0
  46. package/dist/platform/script-result.js +39 -0
  47. package/dist/platform/windows-detector.js +49 -66
  48. package/dist/platform/windows-runner.js +84 -0
  49. package/dist/skills/editmamei-skill.zip +0 -0
  50. package/dist/tools/adjustment-tools.js +80 -132
  51. package/dist/tools/brush-tools.js +4 -15
  52. package/dist/tools/channel-compose-tools.js +4 -23
  53. package/dist/tools/detection-tools.js +59 -47
  54. package/dist/tools/document-tools.js +92 -127
  55. package/dist/tools/filter-tools.js +490 -731
  56. package/dist/tools/group-tools.js +75 -156
  57. package/dist/tools/guide-tools.js +52 -83
  58. package/dist/tools/history-tools.js +29 -63
  59. package/dist/tools/image-placement-tools.js +18 -37
  60. package/dist/tools/image-tools.js +23 -51
  61. package/dist/tools/layer-ordering-tools.js +2 -9
  62. package/dist/tools/layer-properties-tools.js +207 -395
  63. package/dist/tools/layer-tools.js +224 -190
  64. package/dist/tools/layer-transform-tools.js +41 -106
  65. package/dist/tools/metadata-tools.js +2 -9
  66. package/dist/tools/path-tools.js +4 -15
  67. package/dist/tools/portrait-tools.js +2 -5
  68. package/dist/tools/preview-tools.js +59 -72
  69. package/dist/tools/retouch-tools.js +33 -71
  70. package/dist/tools/scene-tools.js +123 -75
  71. package/dist/tools/selection-tools.js +386 -566
  72. package/dist/tools/shape-tools.js +2 -9
  73. package/dist/tools/text-tools.js +24 -57
  74. package/dist/tools/transform-canvas-tools.js +21 -54
  75. package/dist/tools/vector-mask-tools.js +2 -9
  76. package/dist/update/check.js +1 -1
  77. package/dist/utils/jpeg-quality.js +1 -0
  78. package/dist/utils/logger.js +55 -45
  79. package/dist/utils/operation-timeouts.js +1 -0
  80. package/dist/utils/run-script.js +7 -2
  81. package/dist/utils/session-log.js +134 -12
  82. package/dist/utils/template-storage.js +5 -6
  83. package/dist/utils/tool-helpers.js +43 -0
  84. package/dist/version.js +1 -1
  85. package/package.json +10 -3
  86. package/LICENSE +0 -81
  87. package/dist/platform/detector.js +0 -32
  88. package/dist/platform/macos-executor.js +0 -151
  89. package/dist/platform/windows-executor.js +0 -160
  90. /package/dist/platform/{script-executor.js → ports.js} +0 -0
package/NOTICES.md CHANGED
@@ -72,36 +72,12 @@ above.
72
72
  terms (Apache-2.0 §4(d)) are preserved by reference to that source.
73
73
  - Used to detect the 80 COCO object classes.
74
74
 
75
- ### MediaPipe FaceMesh (`models/pro/face_mesh_468.onnx`) — facial landmarks (Pro)
76
-
77
- - **Upstream:** Google MediaPipe Face Landmark / FaceMesh model (the
78
- `face_landmarks_detector` weight from `face_landmarker.task`), trained by Google
79
- on its own data. Converted to ONNX (468-point variant) and redistributed via the
80
- Hugging Face `Heliosoph/mediapipe-face-onnx` repository.
81
- - **License:** Apache-2.0 (the model is permissively licensed by Google;
82
- commercial-safe — see `docs/product/pro-vision-models.md` for the weights-license
83
- diligence). Any upstream `NOTICE`/attribution terms (Apache-2.0 §4(d)) are
84
- preserved by reference to those sources.
85
- - Runs on-device via the same `onnxruntime-web` backend; the image never leaves the
86
- machine. Used by the Pro `ps_detect_landmarks` tool to locate the 468-point
87
- face mesh (eyes, brows, lips, nose, jaw). Ships in the **Pro module**
88
- (`models/pro/`), not the CE tarball.
89
-
90
- ### MobileSAM (`models/pro/mobile_sam_{encoder,decoder}.onnx`) — promptable segmentation (Pro)
91
-
92
- - **Upstream:** MobileSAM ("Faster Segment Anything: Towards Lightweight SAM for
93
- Mobile Applications", Zhang et al.; `ChaoningZhang/MobileSAM`) — a distilled
94
- TinyViT image encoder paired with the Segment Anything prompt-mask decoder,
95
- exported to ONNX (encoder + decoder). Redistributed here as an **FP16** build
96
- converted from the FP32 ONNX export (halves the download; INT8 dynamic
97
- quantization was evaluated and rejected — it collapsed the encoder to
98
- whole-frame masks).
99
- - **License:** Apache-2.0. Any upstream `NOTICE`/attribution terms (Apache-2.0
100
- §4(d)) are preserved by reference to that source.
101
- - Runs on-device via the same `onnxruntime-web` backend; the image never leaves the
102
- machine. Used by the Pro `ps_select_object` tool to turn a box/point prompt into a
103
- pixel-accurate organic selection. Ships in the **Pro module** (`models/pro/`), not
104
- the CE tarball.
75
+ ### Separately licensed modules
76
+
77
+ The Pro module is delivered as its own artifact and bundles additional third-party
78
+ models. Redistribution obligations follow the artifact that carries the files, so
79
+ those notices and their full license texts ship **inside the Pro module**, beside
80
+ its manifest, rather than being enumerated here.
105
81
 
106
82
  ---
107
83
 
package/README.md CHANGED
@@ -2,12 +2,10 @@
2
2
 
3
3
  **Unlock Photoshop with natural-language photo editing.** AI orchestration, not generation.
4
4
 
5
- (Pronounced like _edamame_. Yes, the snack.)
6
-
7
5
  > Independent project, not affiliated with or endorsed by Adobe Inc.
8
6
 
9
7
  [![npm version](https://img.shields.io/npm/v/editmamei.svg)](https://www.npmjs.com/package/editmamei)
10
- [![License](https://img.shields.io/badge/License-Proprietary-blue.svg)](https://editmamei.com/license)
8
+ [![License](https://img.shields.io/badge/License-FSL--1.1--MIT-blue.svg)](LICENSE.md)
11
9
  [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS-lightgrey.svg)]()
12
10
 
13
11
  Editmamei is a Model Context Protocol (MCP) server that drives the Adobe Photoshop you already have. You describe the edit in plain words, your AI assistant plans the steps, and your own copy of Photoshop carries them out with its standard adjustment layers, masks, selections, and filters. The AI directs and Photoshop edits. No generative model touches your pixels, and no image is uploaded to perform an edit.
@@ -76,11 +74,11 @@ These are the choices that shape the surface, and the reason an AI assistant can
76
74
 
77
75
  ## Tool surface
78
76
 
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.
77
+ **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
78
 
81
79
  | Group | Edition | Tools |
82
80
  | --- | --- | --- |
83
- | **Core** | Community | `ping` · `overview` · `list_capabilities` · `undo` · `redo` |
81
+ | **Core** | Community | `ping` · `overview` · `list_capabilities` · `undo` · `redo` · `report_problem` |
84
82
  | **Inspect** | Community | `inspect` · `get_preview` |
85
83
  | **Verify** | Community | `get_histogram` · `compare_regions` · `get_layer_bounds_diff` · `get_selection_preview` |
86
84
  | **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 +86,15 @@ These are the choices that shape the surface, and the reason an AI assistant can
88
86
  | **Adjustments** | Community | `add_adjustment_layer` · `apply_adjustment` |
89
87
  | **Filters** | Community | `apply_filter` |
90
88
  | **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` |
89
+ | **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
90
  | **Masks & paths** | Community | `layer_mask` · `create_clipping_mask` · `path` · `vector_mask` · `apply_image` · `calculations` |
93
91
  | **Type** | Community | `create_text_layer` · `set_text` |
94
- | **Perception** | Community | `detect` · `scene` · `select_by_reference` |
92
+ | **Perception** | Community | `detect` · `read_scene` · `select_by_reference` |
95
93
  | **AI selection** | Community | `select_subject` · `select_sky` |
96
- | **AI selection** | Pro | `select_subject_instance` |
97
- | **Layers (warp)** | Pro | `warp_layer` · `warp_layer_mesh` |
94
+ | **AI selection** | Pro | `select_subject_instance` · `select_object` |
95
+ | **Filters** | Pro | `apply_camera_raw` |
96
+ | **Layers (warp)** | Pro | `warp_layer` · `warp_layer_mesh` · `warp_layer_along` · `warp_layer_region` · `warp_layer_to` |
97
+ | **Perception** | Pro | `edit_object` · `add_text_to_object` · `resolve_placement` |
98
98
  | **Face mesh** | Pro | `detect_landmarks` · `select_face_feature` |
99
99
  | **Templates** | Pro | `template_create_evidence` · `template_save` · `template_list` · `template_apply` · `template_verify` · `template_recall` · `template_delete` |
100
100
  | **Automation** | Pro | `list_actions` · `play_action` · `execute_script` |
@@ -106,6 +106,24 @@ These are the choices that shape the surface, and the reason an AI assistant can
106
106
 
107
107
  The split is detailed in [pro-features.md](https://github.com/editmamei/editmamei-wiki/blob/main/docs/pro-features.md). Pricing is at [editmamei.com](https://editmamei.com).
108
108
 
109
+ This repository holds the Community source. Pro is a separately licensed module and its source is not published here; Community never imports it, so this tree builds and runs on its own.
110
+
111
+ ## Build from source
112
+
113
+ ```bash
114
+ git clone https://github.com/editmamei/editmamei.git
115
+ cd editmamei
116
+ npm install
117
+ npm run build
118
+ npm test
119
+ ```
120
+
121
+ 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
+
123
+ 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.
124
+
125
+ Contributions are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md), which covers the CLA, the branch and pull-request flow, and what to include in a bug report.
126
+
109
127
  ## Configuration
110
128
 
111
129
  Settings live in `~/.editmamei/settings.json`, managed by the `editmamei config` CLI:
@@ -122,17 +140,33 @@ editmamei config set ps_path "/path/to/Photoshop"
122
140
 
123
141
  Editmamei runs on your computer and edits in your own Photoshop. No image content, document data, or file paths are sent to Editmamei's servers. It reports anonymous, content-free usage telemetry (on by default, turn it off with `editmamei config set telemetry.usage false`). When your AI assistant needs to see a result, Editmamei sends it a downscaled preview, the same as dropping a photo into a chat with that assistant. Every field is documented in [privacy.md](https://github.com/editmamei/editmamei-wiki/blob/main/docs/privacy.md).
124
142
 
143
+ The source in this repository is the same code published to npm, so none of the above has to be taken on trust.
144
+
125
145
  ## Docs and support
126
146
 
127
147
  - **Features and pricing:** [editmamei.com](https://editmamei.com)
128
148
  - **Install, getting started, FAQ, Pro features:** [the wiki](https://github.com/editmamei/editmamei-wiki)
129
149
  - **Bugs and feature requests:** [the issue tracker](https://github.com/editmamei/editmamei-wiki/issues). If something's broken, ask your assistant to "report a problem" (or run `editmamei report`) to drop an anonymized diagnostic bundle in your Downloads folder, then attach it to the issue.
130
- - **Security:** see [editmamei.com/security](https://editmamei.com/security) (don't file security issues publicly)
150
+ - **Security:** see [SECURITY.md](SECURITY.md) (don't file security issues publicly)
131
151
 
132
152
  ## License
133
153
 
134
- Proprietary, free to install and use. See [the license](https://editmamei.com/license). Third-party dependencies keep their own licenses, listed in the bundled `NOTICES.md`.
154
+ Editmamei CE is [Fair Source](https://fair.io) software under the
155
+ [Functional Source License, v1.1, MIT Future License](LICENSE.md) (FSL-1.1-MIT).
156
+
157
+ You can read, run, modify, and redistribute the code for almost anything: commercial photo
158
+ editing, internal tools, education, research, security review. The one thing the license
159
+ holds back, for two years per release, is offering the code to others in a commercial
160
+ product or service that competes with Editmamei CE or Pro. If you redistribute the code,
161
+ keep the license and copyright notices with it. Two years after each version ships, that
162
+ version automatically becomes available under plain [MIT](LICENSE.md).
163
+
164
+ This section is a plain-English summary. The [LICENSE](LICENSE.md) file is the license;
165
+ where they differ, the LICENSE file controls.
166
+
167
+ Editmamei is not open source under the OSI definition, and we don't call it that. It is
168
+ source-available, with the full CE stack developed in the open.
135
169
 
136
- ---
170
+ Editmamei Pro is a separate, commercially licensed module; its source is not published.
137
171
 
138
- _Pairs well with: a layered PSD, a willing AI, and a small bowl of edamame._
172
+ Third-party dependencies keep their own licenses, listed in [NOTICES.md](NOTICES.md).
@@ -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 '';
@@ -194,7 +182,7 @@ export function duplicateForOp(opName, applyToActiveLayer) {
194
182
  `;
195
183
  }
196
184
  return `
197
- // Bundle O auto-duplicate-first: the destructive op below will act on
185
+ // Auto-duplicate-first: the destructive op below will act on
198
186
  // a fresh copy of the active layer so the original is preserved.
199
187
  // Undoing the op is as simple as deleting the copy.
200
188
  var __opOriginal = doc.activeLayer;
@@ -398,91 +386,3 @@ function __hoistFromActiveGroupIfNeeded(doc, preMkActive, newLayer, intoActiveGr
398
386
  return false;
399
387
  }
400
388
  `;
401
- export const layerResolveHelpers = `
402
- function __safeGet(fn, fallback) {
403
- try { var v = fn(); return (v === undefined ? fallback : v); }
404
- catch (e) { return fallback; }
405
- }
406
-
407
- function __findLayerPath(layers, target, prefix) {
408
- var n = __safeGet(function () { return layers.length; }, 0);
409
- for (var i = 0; i < n; i++) {
410
- var candidate = __safeGet(function () { return layers[i]; }, null);
411
- if (!candidate) continue;
412
- if (candidate === target) return prefix.concat([i]);
413
- var isGroup = false;
414
- try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
415
- if (isGroup) {
416
- var childLayers = __safeGet(function () { return candidate.layers; }, null);
417
- if (childLayers) {
418
- var found = __findLayerPath(childLayers, target, prefix.concat([i]));
419
- if (found) return found;
420
- }
421
- }
422
- }
423
- return null;
424
- }
425
-
426
- function __findLayerById(layers, id) {
427
- var n = __safeGet(function () { return layers.length; }, 0);
428
- for (var i = 0; i < n; i++) {
429
- var candidate = __safeGet(function () { return layers[i]; }, null);
430
- if (!candidate) continue;
431
- var cid = __safeGet(function () { return candidate.id; }, null);
432
- if (cid === id) return candidate;
433
- var isGroup = false;
434
- try { isGroup = (candidate instanceof LayerSet); } catch (eG) {}
435
- if (isGroup) {
436
- var childLayers = __safeGet(function () { return candidate.layers; }, null);
437
- if (childLayers) {
438
- var found = __findLayerById(childLayers, id);
439
- if (found) return found;
440
- }
441
- }
442
- }
443
- return null;
444
- }
445
-
446
- function __resolvePath(doc, path) {
447
- var current = doc;
448
- for (var i = 0; i < path.length; i++) {
449
- var idx = path[i];
450
- current = __safeGet(function () { return current.layers[idx]; }, null);
451
- if (!current) return null;
452
- }
453
- return current;
454
- }
455
-
456
- function __captureLayerIdentity(doc, layer) {
457
- var id = null;
458
- try { if (typeof layer.id !== 'undefined') { id = layer.id; } } catch (eId) {}
459
- var path = null;
460
- if (id === null) {
461
- path = __findLayerPath(doc.layers, layer, []);
462
- }
463
- return { id: id, path: path };
464
- }
465
-
466
- function __resolveLayerFresh(doc, identity) {
467
- if (identity.id !== null) {
468
- var byId = __findLayerById(doc.layers, identity.id);
469
- if (byId) return byId;
470
- }
471
- if (identity.path) {
472
- return __resolvePath(doc, identity.path);
473
- }
474
- return null;
475
- }
476
-
477
- function __resolveLayerFreshOrActive(doc, identity) {
478
- var resolved = __resolveLayerFresh(doc, identity);
479
- if (resolved) return resolved;
480
- // Identity resolution came up empty even though the write may have
481
- // landed — e.g. Photoshop auto-promoting a Background layer as a side
482
- // effect of the property write. Fall back to the current active layer
483
- // (these setters always operate on doc.activeLayer, and Photoshop keeps
484
- // it pointed at the promoted layer) rather than treating an unresolved
485
- // identity as proof the write failed.
486
- return __safeGet(function () { return doc.activeLayer; }, null);
487
- }
488
- `;
@@ -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(`Photoshop ${info.version} ready for scripting`);
15
16
  return new ExtendScriptPhotoshopAPI(this.connection);
16
17
  }
17
18
  }
@@ -119,17 +120,26 @@ class ExtendScriptPhotoshopAPI {
119
120
  } catch (e) {}
120
121
  }
121
122
 
122
- if (typeof result === 'object' && result !== null) {
123
- return __mcpJsonEncode(result);
124
- }
125
- if (typeof result === 'string') {
126
- // Plain string return — wrap as a JSON string so the Node-side parser
127
- // sees valid JSON. Handlers that expect a string still receive one.
128
- return __mcpJsonEncode(result);
129
- }
130
- return String(result);
123
+ // Every outcome leaves inside an envelope, so the value a script returned
124
+ // is never confused with how it ended. A script that legitimately returns
125
+ // a string beginning with the failure marker used to come back as a thrown
126
+ // error; inside an envelope it round-trips untouched.
127
+ return __mcpJsonEncode({ __em: 1, ok: true, value: result });
131
128
  } catch (error) {
132
- return 'ERROR: ' + (error.message || String(error) || ('code ' + error.number) || 'no error message');
129
+ // Photoshop's error carries more than a message. Passing the number and
130
+ // line through means a failure can be identified rather than string-matched
131
+ // on prose that changes between releases and locales.
132
+ var __message = '';
133
+ try { __message = (error && error.message) ? String(error.message) : String(error); } catch (e) { __message = 'unreadable error'; }
134
+ var __number = null;
135
+ try { if (error && typeof error.number === 'number') __number = error.number; } catch (e) {}
136
+ var __line = null;
137
+ try { if (error && typeof error.line === 'number') __line = error.line; } catch (e) {}
138
+ return __mcpJsonEncode({
139
+ __em: 1,
140
+ ok: false,
141
+ error: { message: __message, number: __number, line: __line }
142
+ });
133
143
  } finally {
134
144
  try { if (__originalRulerUnits !== null) app.preferences.rulerUnits = __originalRulerUnits; } catch (e) {}
135
145
  try { if (__originalTypeUnits !== null) app.preferences.typeUnits = __originalTypeUnits; } catch (e) {}
@@ -138,7 +148,4 @@ class ExtendScriptPhotoshopAPI {
138
148
  })();
139
149
  `.trim();
140
150
  }
141
- getAPIType() {
142
- return 'ExtendScript';
143
- }
144
151
  }
@@ -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