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.
- package/NOTICES.md +6 -30
- package/README.md +47 -13
- package/dist/api/extendscript/_helpers.js +1 -101
- package/dist/api/photoshop-api.js +23 -16
- package/dist/api/snippet-client.js +38 -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/core/server.js +60 -184
- package/dist/core/session.js +9 -10
- package/dist/core/tool-registry.js +4 -4
- package/dist/delivery/client.js +50 -1
- package/dist/delivery/provision.js +18 -1
- package/dist/delivery/signing.js +38 -1
- package/dist/delivery/store.js +157 -16
- package/dist/detection/detect-active-doc.js +42 -11
- package/dist/detection/detection-client.js +3 -3
- package/dist/detection/face-detector.js +2 -2
- package/dist/detection/geometry.js +14 -0
- package/dist/detection/landmark-detection-client.js +3 -3
- package/dist/detection/mesh-face.js +2 -3
- package/dist/detection/object-detector.js +2 -2
- package/dist/detection/runtime.js +14 -3
- package/dist/detection/sam-segmenter.js +69 -26
- package/dist/kernel/kernel.js +5 -9
- package/dist/kernel/module-lifecycle.js +187 -0
- package/dist/license/entitlement.js +13 -3
- package/dist/license/store.js +8 -1
- package/dist/modules/ce/index.js +1 -1
- package/dist/perception/facets.js +19 -1
- package/dist/perception/grounding-anchors.js +2 -12
- package/dist/perception/grounding-locate.js +1 -9
- package/dist/perception/grounding-review-crop.js +4 -29
- package/dist/perception/overlay.js +62 -0
- package/dist/perception/pixel-identity.js +41 -0
- package/dist/perception/region-precompute.js +96 -20
- package/dist/perception/scene-model.js +93 -50
- package/dist/perception/select-recipes.js +109 -68
- package/dist/perception/sky-ground-flood.js +0 -5
- package/dist/perception/sky-mask-transfer.js +73 -14
- package/dist/platform/connection.js +60 -43
- package/dist/platform/host-platform.js +16 -0
- package/dist/platform/macos-detector.js +41 -70
- package/dist/platform/macos-runner.js +94 -0
- package/dist/platform/script-queue.js +68 -0
- package/dist/platform/script-result.js +39 -0
- package/dist/platform/windows-detector.js +49 -66
- package/dist/platform/windows-runner.js +84 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +80 -132
- package/dist/tools/brush-tools.js +4 -15
- package/dist/tools/channel-compose-tools.js +4 -23
- package/dist/tools/detection-tools.js +59 -47
- package/dist/tools/document-tools.js +92 -127
- package/dist/tools/filter-tools.js +490 -731
- package/dist/tools/group-tools.js +75 -156
- package/dist/tools/guide-tools.js +52 -83
- package/dist/tools/history-tools.js +29 -63
- package/dist/tools/image-placement-tools.js +18 -37
- package/dist/tools/image-tools.js +23 -51
- package/dist/tools/layer-ordering-tools.js +2 -9
- package/dist/tools/layer-properties-tools.js +207 -395
- package/dist/tools/layer-tools.js +224 -190
- package/dist/tools/layer-transform-tools.js +41 -106
- package/dist/tools/metadata-tools.js +2 -9
- package/dist/tools/path-tools.js +4 -15
- package/dist/tools/portrait-tools.js +2 -5
- package/dist/tools/preview-tools.js +59 -72
- package/dist/tools/retouch-tools.js +33 -71
- package/dist/tools/scene-tools.js +123 -75
- package/dist/tools/selection-tools.js +386 -566
- package/dist/tools/shape-tools.js +2 -9
- package/dist/tools/text-tools.js +24 -57
- package/dist/tools/transform-canvas-tools.js +21 -54
- package/dist/tools/vector-mask-tools.js +2 -9
- package/dist/update/check.js +1 -1
- package/dist/utils/jpeg-quality.js +1 -0
- package/dist/utils/logger.js +55 -45
- package/dist/utils/operation-timeouts.js +1 -0
- package/dist/utils/run-script.js +7 -2
- package/dist/utils/session-log.js +134 -12
- package/dist/utils/template-storage.js +5 -6
- package/dist/utils/tool-helpers.js +43 -0
- package/dist/version.js +1 -1
- package/package.json +10 -3
- package/LICENSE +0 -81
- package/dist/platform/detector.js +0 -32
- package/dist/platform/macos-executor.js +0 -151
- package/dist/platform/windows-executor.js +0 -160
- /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
|
-
###
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
[](https://www.npmjs.com/package/editmamei)
|
|
10
|
-
[](LICENSE.md)
|
|
11
9
|
[]()
|
|
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
|
-
**
|
|
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` · `
|
|
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` · `
|
|
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
|
-
| **
|
|
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 [
|
|
150
|
+
- **Security:** see [SECURITY.md](SECURITY.md) (don't file security issues publicly)
|
|
131
151
|
|
|
132
152
|
## License
|
|
133
153
|
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
10
|
+
const info = await this.connection.ensureDetected();
|
|
11
11
|
if (!info) {
|
|
12
|
-
throw new Error('Photoshop info not available
|
|
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.
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
Binary file
|