editmamei 0.23.0 → 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.
- package/README.md +8 -6
- package/dist/api/extendscript/_helpers.js +0 -100
- package/dist/api/photoshop-api.js +4 -3
- 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 +56 -180
- 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 +149 -16
- package/dist/detection/detect-active-doc.js +41 -10
- 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 +181 -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 +85 -17
- package/dist/perception/scene-model.js +93 -50
- package/dist/perception/select-recipes.js +107 -66
- package/dist/perception/sky-ground-flood.js +0 -5
- package/dist/perception/sky-mask-transfer.js +72 -13
- package/dist/platform/connection.js +42 -11
- package/dist/platform/executor-queue.js +68 -0
- package/dist/platform/macos-detector.js +2 -1
- package/dist/platform/macos-executor.js +7 -62
- package/dist/platform/windows-detector.js +14 -7
- package/dist/platform/windows-executor.js +9 -64
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +62 -130
- package/dist/tools/brush-tools.js +3 -14
- package/dist/tools/channel-compose-tools.js +4 -23
- package/dist/tools/detection-tools.js +59 -47
- package/dist/tools/document-tools.js +87 -129
- package/dist/tools/filter-tools.js +489 -730
- 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 +95 -186
- package/dist/tools/layer-transform-tools.js +41 -106
- package/dist/tools/metadata-tools.js +2 -9
- package/dist/tools/path-tools.js +3 -14
- package/dist/tools/portrait-tools.js +2 -5
- package/dist/tools/preview-tools.js +51 -64
- package/dist/tools/retouch-tools.js +32 -70
- package/dist/tools/scene-tools.js +94 -73
- package/dist/tools/selection-tools.js +305 -563
- 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/utils/jpeg-quality.js +1 -0
- package/dist/utils/logger.js +3 -0
- package/dist/utils/operation-timeouts.js +1 -0
- package/dist/utils/run-script.js +7 -2
- package/dist/utils/session-log.js +133 -11
- package/dist/utils/template-storage.js +5 -6
- package/dist/utils/tool-helpers.js +37 -0
- package/dist/version.js +1 -1
- 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
|
-
**
|
|
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` · `
|
|
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` · `
|
|
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
|
-
| **
|
|
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 '';
|
|
@@ -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(`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:
|
|
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
|
package/dist/core/server.js
CHANGED
|
@@ -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
|
|
17
|
-
import {
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
68
|
-
moduleSkipReason
|
|
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
|
|
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
|
|
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.
|
|
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 &&
|
|
214
|
-
? new GoSnippetClient({ binaryPath: join(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/delivery/client.js
CHANGED
|
@@ -11,6 +11,7 @@ export class DeliveryError extends Error {
|
|
|
11
11
|
this.name = 'DeliveryError';
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
14
15
|
const USER_AGENT = 'editmamei-delivery-client/1';
|
|
15
16
|
const HEADER_KEY = 'x-editmamei-license-key';
|
|
16
17
|
const defaultFetch = (url, init) => fetch(url, init);
|
|
@@ -23,6 +24,7 @@ export class DeliveryClient {
|
|
|
23
24
|
retry;
|
|
24
25
|
sleep;
|
|
25
26
|
minIntervalMs;
|
|
27
|
+
maxArtifactBytes;
|
|
26
28
|
lastRequestAt = 0;
|
|
27
29
|
constructor(opts = {}) {
|
|
28
30
|
this.cfg = opts.config ?? resolveDeliveryConfig();
|
|
@@ -30,6 +32,7 @@ export class DeliveryClient {
|
|
|
30
32
|
this.retry = { ...DEFAULT_RETRY, ...opts.retry };
|
|
31
33
|
this.sleep = opts.sleep ?? defaultSleep;
|
|
32
34
|
this.minIntervalMs = opts.minRequestIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
35
|
+
this.maxArtifactBytes = opts.maxArtifactBytes ?? MAX_ARTIFACT_BYTES;
|
|
33
36
|
if (!this.cfg.baseUrl) {
|
|
34
37
|
throw new DeliveryError('Delivery endpoint is not configured. Set EDITMAMEI_DELIVERY_URL ' +
|
|
35
38
|
'(e.g. http://localhost:8787 for a local wrangler dev Worker).', 0, 'not_configured');
|
|
@@ -52,9 +55,55 @@ export class DeliveryClient {
|
|
|
52
55
|
const res = await this.get(`/v1/modules/${sku}/${version}`, key);
|
|
53
56
|
if (!res.ok)
|
|
54
57
|
throw this.errorFor(res.status, await safeText(res), this.retryAfterMsOf(res));
|
|
55
|
-
|
|
58
|
+
const declared = Number(res.headers?.get('content-length'));
|
|
59
|
+
if (Number.isFinite(declared) && declared > this.maxArtifactBytes) {
|
|
60
|
+
throw this.oversizeError(res.status, declared);
|
|
61
|
+
}
|
|
62
|
+
const body = res.body;
|
|
63
|
+
if (!body) {
|
|
64
|
+
const buf = new Uint8Array(await res.arrayBuffer());
|
|
65
|
+
if (buf.length > this.maxArtifactBytes)
|
|
66
|
+
throw this.oversizeError(res.status, buf.length);
|
|
67
|
+
return buf;
|
|
68
|
+
}
|
|
69
|
+
return await this.readCapped(body, res.status);
|
|
56
70
|
});
|
|
57
71
|
}
|
|
72
|
+
async readCapped(body, status) {
|
|
73
|
+
const reader = body.getReader();
|
|
74
|
+
const chunks = [];
|
|
75
|
+
let total = 0;
|
|
76
|
+
try {
|
|
77
|
+
for (;;) {
|
|
78
|
+
const { done, value } = await reader.read();
|
|
79
|
+
if (done)
|
|
80
|
+
break;
|
|
81
|
+
total += value.byteLength;
|
|
82
|
+
if (total > this.maxArtifactBytes) {
|
|
83
|
+
try {
|
|
84
|
+
await reader.cancel('artifact exceeds byte cap');
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
}
|
|
88
|
+
throw this.oversizeError(status, total);
|
|
89
|
+
}
|
|
90
|
+
chunks.push(value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
reader.releaseLock();
|
|
95
|
+
}
|
|
96
|
+
const out = new Uint8Array(total);
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (const chunk of chunks) {
|
|
99
|
+
out.set(chunk, offset);
|
|
100
|
+
offset += chunk.byteLength;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
oversizeError(status, sizeBytes) {
|
|
105
|
+
return new DeliveryError(`Artifact exceeds the ${this.maxArtifactBytes}-byte cap (${sizeBytes} bytes) — refusing to buffer further.`, status, 'oversize');
|
|
106
|
+
}
|
|
58
107
|
async withRetry(fn) {
|
|
59
108
|
let lastErr;
|
|
60
109
|
for (let attempt = 1; attempt <= this.retry.attempts; attempt++) {
|