editmamei 1.4.0 → 1.5.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/README.md +6 -6
- 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/cli/help.js +1 -1
- package/dist/core/server.js +110 -13
- package/dist/core/tool-groups.js +1 -0
- package/dist/core/tool-tiers.js +1 -0
- package/dist/install-channel.js +52 -2
- package/dist/kernel/module-lifecycle.js +44 -17
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/activity.js +149 -0
- package/dist/telemetry/client.js +115 -5
- package/dist/telemetry/events.js +43 -1
- package/dist/tools/document-tools.js +23 -1
- package/dist/update/check.js +24 -10
- package/dist/utils/session-log.js +3 -3
- package/dist/utils/xmp-crs.js +682 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](LICENSE.md)
|
|
10
10
|
[](docs/installation.md)
|
|
11
11
|
|
|
12
|
-
Editmamei is a Model Context Protocol (MCP) server that drives
|
|
12
|
+
Editmamei is a Model Context Protocol (MCP) server that drives desktop Adobe Photoshop. You describe the edit in plain words, your AI assistant plans the steps, and 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.
|
|
13
13
|
|
|
14
14
|
It serves photographers and retouchers who want to edit by conversation just as much as developers wiring Photoshop into an AI pipeline. To learn more or get started, visit **[editmamei.com](https://editmamei.com)**.
|
|
15
15
|
|
|
@@ -47,7 +47,7 @@ Then restart your AI client and ask it: _"Is Photoshop connected?"_
|
|
|
47
47
|
|
|
48
48
|
Prefer not to use a terminal? Claude Desktop users can install the one-click [`.mcpb` extension](https://github.com/editmamei/editmamei/releases/latest/download/editmamei.mcpb) directly (no system Node required, Claude Desktop ships its own runtime). Full walkthrough in the [install guide](docs/installation.md).
|
|
49
49
|
|
|
50
|
-
**Requirements:** Adobe Photoshop 2026 (v27.x), Windows 10/11 or macOS 13+, Node.js 22+ (for the npm path), and an MCP-compatible AI client (Claude Desktop, Cursor, Claude Code).
|
|
50
|
+
**Requirements:** Adobe Photoshop 2026 (v27.x), Windows 10/11 or macOS 13+, Node.js 22+ (for the npm path), and an MCP-compatible AI client (Claude Desktop, Cursor, Claude Code). Editmamei ships no model of its own and runs on whichever client you connect, so every edit spends that client's tokens.
|
|
51
51
|
|
|
52
52
|
## A session, end to end
|
|
53
53
|
|
|
@@ -75,7 +75,7 @@ These are the choices that shape the surface, and the reason an AI assistant can
|
|
|
75
75
|
|
|
76
76
|
## Tool surface
|
|
77
77
|
|
|
78
|
-
**
|
|
78
|
+
**83 tools across 16 capability groups** (62 Community, 21 Pro). Every tool is namespaced `ps_*` and discoverable at runtime via `tools/list`. Community tools ship in both editions; Pro tools unlock with a license.
|
|
79
79
|
|
|
80
80
|
| Group | Edition | Tools |
|
|
81
81
|
| --- | --- | --- |
|
|
@@ -94,7 +94,7 @@ These are the choices that shape the surface, and the reason an AI assistant can
|
|
|
94
94
|
| **AI selection** | Community | `select_subject` · `select_sky` · `replace_sky` |
|
|
95
95
|
| **Automation** | Community | `sequence` |
|
|
96
96
|
| **AI selection** | Pro | `select_subject_instance` · `select_object` |
|
|
97
|
-
| **Filters** | Pro | `apply_camera_raw` |
|
|
97
|
+
| **Filters** | Pro | `apply_camera_raw` · `develop_raw` |
|
|
98
98
|
| **Layers (warp)** | Pro | `warp_layer` |
|
|
99
99
|
| **Perception** | Pro | `edit_object` · `add_text_to_object` · `resolve_placement` |
|
|
100
100
|
| **Face mesh** | Pro | `detect_landmarks` · `select_face_feature` |
|
|
@@ -132,7 +132,7 @@ Settings live in `~/.editmamei/settings.json`, managed by the `editmamei config`
|
|
|
132
132
|
|
|
133
133
|
```bash
|
|
134
134
|
editmamei config list
|
|
135
|
-
editmamei config set telemetry.usage false # turn off
|
|
135
|
+
editmamei config set telemetry.usage false # turn off usage telemetry
|
|
136
136
|
editmamei config set ps_path "/path/to/Photoshop"
|
|
137
137
|
```
|
|
138
138
|
|
|
@@ -140,7 +140,7 @@ editmamei config set ps_path "/path/to/Photoshop"
|
|
|
140
140
|
|
|
141
141
|
## Privacy
|
|
142
142
|
|
|
143
|
-
Editmamei runs on your computer and edits in
|
|
143
|
+
Editmamei runs on your computer and edits in desktop Photoshop. No image content, document data, or file paths are sent to Editmamei's servers. It reports 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](docs/privacy.md), including [your rights, and the legal basis](docs/privacy.md#your-rights-and-the-legal-basis).
|
|
144
144
|
|
|
145
145
|
The source in this repository is the same code published to npm, so none of the above has to be taken on trust.
|
|
146
146
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/cli/help.js
CHANGED
|
@@ -33,7 +33,7 @@ Install options:
|
|
|
33
33
|
|
|
34
34
|
Config examples:
|
|
35
35
|
editmamei config list Show all settings
|
|
36
|
-
editmamei config set telemetry.usage false Opt out of
|
|
36
|
+
editmamei config set telemetry.usage false Opt out of usage telemetry
|
|
37
37
|
editmamei config set telemetry.diagnostics true Opt in to sanitized diagnostic detail
|
|
38
38
|
|
|
39
39
|
Report options:
|
package/dist/core/server.js
CHANGED
|
@@ -8,13 +8,14 @@ import { groupOf, GROUPS } from './tool-groups.js';
|
|
|
8
8
|
import { EDITION } from '../edition.js';
|
|
9
9
|
import { VERSION } from '../version.js';
|
|
10
10
|
import { Session } from './session.js';
|
|
11
|
-
import { SessionLog, classifyError, NO_ERROR_TEXT_CLASS } from '../utils/session-log.js';
|
|
11
|
+
import { SessionLog, classifyError, computeResultBytes, NO_ERROR_TEXT_CLASS, } from '../utils/session-log.js';
|
|
12
12
|
import { loadSettings, applyTelemetryEnvOverrides, applyUpdateCheckEnvOverride, } from './settings.js';
|
|
13
13
|
import { TelemetryClient } from '../telemetry/client.js';
|
|
14
14
|
import { resolveInstallChannel } from '../install-channel.js';
|
|
15
|
-
import {
|
|
15
|
+
import { checkForUpdateWithStatus, shouldCheckForUpdate, } from '../update/check.js';
|
|
16
16
|
import { previousSessionFailureCounts, relevantFixes } from '../update/session-fixes.js';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
18
19
|
import { GoSnippetClient, coreBinaryName } from '../api/snippet-client.js';
|
|
19
20
|
import { isProEntitled } from '../license/entitlement.js';
|
|
20
21
|
import { createPingLicenseRefresher } from '../license/ping-refresh.js';
|
|
@@ -42,9 +43,20 @@ function warnLogScriptOnErrorOnce(logger) {
|
|
|
42
43
|
export function __resetLogScriptOnErrorWarnForTests() {
|
|
43
44
|
logScriptOnErrorWarned = false;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
function hashRetryKey(tool, args) {
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.stringify({ tool, args });
|
|
49
|
+
return createHash('sha256').update(json).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const FIRST_RUN_DISCLOSURE = 'First run: Editmamei collects content-free usage telemetry (tool name, ' +
|
|
56
|
+
'success, duration, bytes returned, version/edition/OS/PS-version, install channel, ' +
|
|
57
|
+
'which AI client connected, Node/OS/architecture versions, and per-session counts like ' +
|
|
58
|
+
'edits made and retries) to find what breaks. It never sends image content, file paths, ' +
|
|
59
|
+
'or tool arguments. The only identifier is a random install ID. Opt out anytime: ' +
|
|
48
60
|
'`editmamei config set telemetry.usage false` (or edit ~/.editmamei/settings.json). ' +
|
|
49
61
|
'Opt in to sanitized diagnostics: `editmamei config set telemetry.diagnostics true`.';
|
|
50
62
|
export const BACKGROUND_VERSION_PROBE_TIMEOUT_MS = 3_000;
|
|
@@ -62,13 +74,17 @@ export class EditmameiServer {
|
|
|
62
74
|
}
|
|
63
75
|
psVersion = null;
|
|
64
76
|
lastPingReachedPs = null;
|
|
77
|
+
lastTelemetryCallKeyHash = null;
|
|
78
|
+
lastPsLocale = null;
|
|
79
|
+
lastDocDepth = null;
|
|
80
|
+
lastDocMode = null;
|
|
65
81
|
liveVersionResolutionAttempted = false;
|
|
66
82
|
updateInfo = null;
|
|
67
83
|
updateCheck = null;
|
|
68
84
|
updateNoticeShown = false;
|
|
69
85
|
snippetClient = new GoSnippetClient();
|
|
70
86
|
refreshLicenseOnPing = createPingLicenseRefresher();
|
|
71
|
-
constructor() {
|
|
87
|
+
constructor(opts = {}) {
|
|
72
88
|
this.logger = new Logger('EditmameiServer');
|
|
73
89
|
this.session = new Session();
|
|
74
90
|
this.sessionLog = new SessionLog(this.session.getSessionId());
|
|
@@ -84,10 +100,18 @@ export class EditmameiServer {
|
|
|
84
100
|
});
|
|
85
101
|
if (created)
|
|
86
102
|
this.logger.info(FIRST_RUN_DISCLOSURE);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
103
|
+
const shouldCheckForUpdateFn = opts.shouldCheckForUpdate ?? shouldCheckForUpdate;
|
|
104
|
+
const checkForUpdateWithStatusFn = opts.checkForUpdateWithStatus ?? checkForUpdateWithStatus;
|
|
105
|
+
if (shouldCheckForUpdateFn(effectiveSettings.update_check)) {
|
|
106
|
+
this.updateCheck = checkForUpdateWithStatusFn()
|
|
107
|
+
.then((result) => {
|
|
108
|
+
this.updateInfo = result.info;
|
|
109
|
+
if (result.status === 'newer')
|
|
110
|
+
this.telemetry.setBehindLatest(true);
|
|
111
|
+
else if (result.status === 'current')
|
|
112
|
+
this.telemetry.setBehindLatest(false);
|
|
113
|
+
})
|
|
114
|
+
.catch(() => undefined);
|
|
91
115
|
}
|
|
92
116
|
warnLogScriptOnErrorOnce(this.logger);
|
|
93
117
|
this.toolRegistry = new ToolRegistry({
|
|
@@ -97,22 +121,33 @@ export class EditmameiServer {
|
|
|
97
121
|
this.resolveLiveVersionInBackground();
|
|
98
122
|
}
|
|
99
123
|
}
|
|
100
|
-
|
|
124
|
+
const resultBytes = computeResultBytes(entry.result);
|
|
125
|
+
void this.sessionLog
|
|
126
|
+
.append({
|
|
101
127
|
tool: entry.tool,
|
|
102
128
|
args: entry.args,
|
|
103
129
|
success: entry.success,
|
|
104
130
|
duration_ms: entry.duration_ms,
|
|
105
131
|
...(entry.error ? { error: entry.error } : {}),
|
|
106
|
-
}, entry.result)
|
|
132
|
+
}, entry.result, resultBytes)
|
|
133
|
+
.catch((err) => {
|
|
134
|
+
this.logger.debug(`session log append failed: ${String(err)}`);
|
|
135
|
+
});
|
|
107
136
|
const telemetrySuccess = entry.tool === 'ps_ping' && this.lastPingReachedPs === false ? false : entry.success;
|
|
108
137
|
const isPingDowngrade = entry.tool === 'ps_ping' && !telemetrySuccess && entry.error === undefined;
|
|
109
138
|
const errorClass = classifyError(entry.error) ??
|
|
110
139
|
(telemetrySuccess ? null : isPingDowngrade ? 'ps_not_running' : NO_ERROR_TEXT_CLASS);
|
|
140
|
+
const telemetryCallKeyHash = hashRetryKey(entry.tool, entry.args);
|
|
141
|
+
const isTelemetryRetry = telemetryCallKeyHash !== null && telemetryCallKeyHash === this.lastTelemetryCallKeyHash;
|
|
142
|
+
if (telemetryCallKeyHash !== null)
|
|
143
|
+
this.lastTelemetryCallKeyHash = telemetryCallKeyHash;
|
|
111
144
|
this.telemetry.recordCall({
|
|
112
145
|
tool: entry.tool,
|
|
113
146
|
success: telemetrySuccess,
|
|
114
147
|
duration_ms: entry.duration_ms,
|
|
115
148
|
error_class: errorClass,
|
|
149
|
+
result_bytes: resultBytes,
|
|
150
|
+
retry: isTelemetryRetry,
|
|
116
151
|
});
|
|
117
152
|
if (!telemetrySuccess) {
|
|
118
153
|
this.telemetry.recordDiagnostic({
|
|
@@ -122,6 +157,9 @@ export class EditmameiServer {
|
|
|
122
157
|
(isPingDowngrade
|
|
123
158
|
? 'ps_ping did not reach Photoshop'
|
|
124
159
|
: 'tool reported failure with no message'),
|
|
160
|
+
...(this.lastPsLocale !== null ? { ps_locale: this.lastPsLocale } : {}),
|
|
161
|
+
...(this.lastDocDepth !== null ? { doc_depth: this.lastDocDepth } : {}),
|
|
162
|
+
...(this.lastDocMode !== null ? { doc_mode: this.lastDocMode } : {}),
|
|
125
163
|
});
|
|
126
164
|
}
|
|
127
165
|
},
|
|
@@ -136,6 +174,21 @@ export class EditmameiServer {
|
|
|
136
174
|
});
|
|
137
175
|
const srv = this.server;
|
|
138
176
|
this.sessionLog.setMcpClientGetter(() => srv.getClientVersion?.() ?? null);
|
|
177
|
+
let clientConnectedSent = false;
|
|
178
|
+
this.server.oninitialized = () => {
|
|
179
|
+
if (clientConnectedSent)
|
|
180
|
+
return;
|
|
181
|
+
clientConnectedSent = true;
|
|
182
|
+
const clientInfo = this.server.getClientVersion();
|
|
183
|
+
const caps = this.server.getClientCapabilities();
|
|
184
|
+
this.telemetry.recordClientConnected({
|
|
185
|
+
clientName: clientInfo?.name,
|
|
186
|
+
clientVersion: clientInfo?.version,
|
|
187
|
+
capSampling: caps?.sampling != null,
|
|
188
|
+
capElicitation: caps?.elicitation != null,
|
|
189
|
+
capRoots: caps?.roots != null,
|
|
190
|
+
});
|
|
191
|
+
};
|
|
139
192
|
this.registerTools();
|
|
140
193
|
this.setupHandlers();
|
|
141
194
|
}
|
|
@@ -223,6 +276,7 @@ export class EditmameiServer {
|
|
|
223
276
|
logger: this.logger,
|
|
224
277
|
assertToolsClassified: () => this.assertToolsClassified(),
|
|
225
278
|
classifyTool: (name) => this.classifyTool(name),
|
|
279
|
+
onModuleUpdate: (outcome) => this.telemetry.setModuleUpdate(outcome),
|
|
226
280
|
});
|
|
227
281
|
const proModule = this.moduleLifecycle.resolveProModule();
|
|
228
282
|
this.kernel = new Kernel({
|
|
@@ -332,6 +386,11 @@ export class EditmameiServer {
|
|
|
332
386
|
clearPendingRawDevelop();
|
|
333
387
|
}
|
|
334
388
|
}
|
|
389
|
+
else if (name === 'ps_develop_raw') {
|
|
390
|
+
const sc = result.structuredContent;
|
|
391
|
+
if (sc?.opened === true)
|
|
392
|
+
clearPendingRawDevelop();
|
|
393
|
+
}
|
|
335
394
|
else if (name === 'ps_apply_camera_raw' || name === 'ps_close_document') {
|
|
336
395
|
clearPendingRawDevelop();
|
|
337
396
|
}
|
|
@@ -369,9 +428,14 @@ export class EditmameiServer {
|
|
|
369
428
|
const connection = this.session.getConnection();
|
|
370
429
|
let version = 'Unknown';
|
|
371
430
|
let actionSetsCount = 0;
|
|
431
|
+
let actionSetsObserved = false;
|
|
372
432
|
let openDocuments = [];
|
|
433
|
+
let psLocale = null;
|
|
434
|
+
let docDepth = null;
|
|
435
|
+
let docMode = null;
|
|
373
436
|
const degraded = [];
|
|
374
437
|
let versionIsLive = false;
|
|
438
|
+
let pingStateObserved = false;
|
|
375
439
|
try {
|
|
376
440
|
const detected = await connection.getVersion();
|
|
377
441
|
if (detected)
|
|
@@ -381,6 +445,8 @@ export class EditmameiServer {
|
|
|
381
445
|
}
|
|
382
446
|
if (connection.getPhotoshopInfo() !== null && !(await connection.isCurrentlyRunning())) {
|
|
383
447
|
this.lastPingReachedPs = false;
|
|
448
|
+
this.lastDocDepth = null;
|
|
449
|
+
this.lastDocMode = null;
|
|
384
450
|
return {
|
|
385
451
|
content: [
|
|
386
452
|
{
|
|
@@ -406,6 +472,8 @@ export class EditmameiServer {
|
|
|
406
472
|
if (!alive) {
|
|
407
473
|
const reason = unsupportedHostReason();
|
|
408
474
|
this.lastPingReachedPs = false;
|
|
475
|
+
this.lastDocDepth = null;
|
|
476
|
+
this.lastDocMode = null;
|
|
409
477
|
return {
|
|
410
478
|
content: [
|
|
411
479
|
{
|
|
@@ -430,6 +498,8 @@ export class EditmameiServer {
|
|
|
430
498
|
catch (err) {
|
|
431
499
|
this.logger.warn(`pingState snippet failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
432
500
|
this.lastPingReachedPs = false;
|
|
501
|
+
this.lastDocDepth = null;
|
|
502
|
+
this.lastDocMode = null;
|
|
433
503
|
return {
|
|
434
504
|
content: [{ type: 'text', text: 'Photoshop did not respond' + update.note }],
|
|
435
505
|
structuredContent: {
|
|
@@ -439,19 +509,36 @@ export class EditmameiServer {
|
|
|
439
509
|
},
|
|
440
510
|
};
|
|
441
511
|
}
|
|
512
|
+
pingStateObserved = true;
|
|
442
513
|
if (state.version) {
|
|
443
514
|
version = state.version;
|
|
444
515
|
versionIsLive = true;
|
|
445
516
|
}
|
|
446
|
-
if (typeof state.action_sets_count === 'number')
|
|
517
|
+
if (typeof state.action_sets_count === 'number') {
|
|
447
518
|
actionSetsCount = state.action_sets_count;
|
|
519
|
+
actionSetsObserved = true;
|
|
520
|
+
}
|
|
448
521
|
if (Array.isArray(state.open_documents))
|
|
449
522
|
openDocuments = state.open_documents;
|
|
523
|
+
if (typeof state.locale === 'string')
|
|
524
|
+
psLocale = state.locale;
|
|
525
|
+
if (state.doc_depth === 8 || state.doc_depth === 16 || state.doc_depth === 32) {
|
|
526
|
+
docDepth = state.doc_depth;
|
|
527
|
+
}
|
|
528
|
+
if (state.doc_mode === 'rgb' ||
|
|
529
|
+
state.doc_mode === 'cmyk' ||
|
|
530
|
+
state.doc_mode === 'lab' ||
|
|
531
|
+
state.doc_mode === 'grayscale' ||
|
|
532
|
+
state.doc_mode === 'other') {
|
|
533
|
+
docMode = state.doc_mode;
|
|
534
|
+
}
|
|
450
535
|
}
|
|
451
536
|
let userTemplates = 0;
|
|
537
|
+
let templatesObserved = false;
|
|
452
538
|
try {
|
|
453
539
|
const templates = await listTemplates();
|
|
454
540
|
userTemplates = templates.length;
|
|
541
|
+
templatesObserved = true;
|
|
455
542
|
}
|
|
456
543
|
catch (err) {
|
|
457
544
|
this.logger.warn(`listTemplates failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -464,6 +551,16 @@ export class EditmameiServer {
|
|
|
464
551
|
this.psVersion = version;
|
|
465
552
|
this.telemetry.onPsVersionResolved();
|
|
466
553
|
}
|
|
554
|
+
if (psLocale !== null)
|
|
555
|
+
this.lastPsLocale = psLocale;
|
|
556
|
+
if (pingStateObserved) {
|
|
557
|
+
this.lastDocDepth = docDepth;
|
|
558
|
+
this.lastDocMode = docMode;
|
|
559
|
+
}
|
|
560
|
+
this.telemetry.setInstallAssets({
|
|
561
|
+
...(templatesObserved ? { templates_saved: userTemplates } : {}),
|
|
562
|
+
...(actionSetsObserved ? { action_sets: actionSetsCount } : {}),
|
|
563
|
+
});
|
|
467
564
|
this.lastPingReachedPs = true;
|
|
468
565
|
const degradedNote = degraded.length ? ` (degraded: ${degraded.join(', ')})` : '';
|
|
469
566
|
return {
|
package/dist/core/tool-groups.js
CHANGED
package/dist/core/tool-tiers.js
CHANGED
package/dist/install-channel.js
CHANGED
|
@@ -1,8 +1,58 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
1
2
|
import { EDITION } from './edition.js';
|
|
2
|
-
|
|
3
|
+
function pathSegments(path) {
|
|
4
|
+
return path
|
|
5
|
+
.split(/[\\/]/)
|
|
6
|
+
.filter((s) => s.length > 0)
|
|
7
|
+
.map((s) => s.toLowerCase());
|
|
8
|
+
}
|
|
9
|
+
function defaultRealpath(p) {
|
|
10
|
+
try {
|
|
11
|
+
return realpathSync.native(p);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return p;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function safeRealpath(p, realpath) {
|
|
18
|
+
try {
|
|
19
|
+
return realpath(p);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return p;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function prefixMatches(execDirSegments, entryPrefixSegments) {
|
|
26
|
+
return (execDirSegments.length > 0 && execDirSegments.every((seg, i) => entryPrefixSegments[i] === seg));
|
|
27
|
+
}
|
|
28
|
+
export function resolveInstallChannel(env = process.env, edition = EDITION, argv1 = process.argv[1], execPath = process.execPath, realpath = defaultRealpath) {
|
|
3
29
|
if (edition === 'dev')
|
|
4
30
|
return 'dev';
|
|
5
31
|
if (env.EDITMAMEI_INSTALL_CHANNEL === 'mcpb')
|
|
6
32
|
return 'mcpb';
|
|
7
|
-
|
|
33
|
+
const trimmedArgv1 = argv1?.trim();
|
|
34
|
+
if (!trimmedArgv1)
|
|
35
|
+
return 'unknown';
|
|
36
|
+
const resolvedArgv1 = safeRealpath(trimmedArgv1, realpath);
|
|
37
|
+
const segments = pathSegments(resolvedArgv1);
|
|
38
|
+
if (segments.includes('_npx'))
|
|
39
|
+
return 'npx';
|
|
40
|
+
const nodeModulesIdx = segments.indexOf('node_modules');
|
|
41
|
+
if (nodeModulesIdx === -1)
|
|
42
|
+
return 'source';
|
|
43
|
+
const precedingSegment = nodeModulesIdx > 0 ? segments[nodeModulesIdx - 1] : undefined;
|
|
44
|
+
if (precedingSegment === 'lib' || precedingSegment === 'npm')
|
|
45
|
+
return 'npm_global';
|
|
46
|
+
const resolvedExecPath = execPath === undefined ? undefined : safeRealpath(execPath, realpath);
|
|
47
|
+
const resolvedExecDir = pathSegments(resolvedExecPath ?? '').slice(0, -1);
|
|
48
|
+
const resolvedEntryPrefix = segments.slice(0, nodeModulesIdx);
|
|
49
|
+
const rawExecDir = execPath === undefined ? [] : pathSegments(execPath).slice(0, -1);
|
|
50
|
+
const rawArgv1Segments = pathSegments(trimmedArgv1);
|
|
51
|
+
const rawNodeModulesIdx = rawArgv1Segments.indexOf('node_modules');
|
|
52
|
+
const rawEntryPrefix = rawNodeModulesIdx === -1 ? [] : rawArgv1Segments.slice(0, rawNodeModulesIdx);
|
|
53
|
+
const underExecDir = prefixMatches(resolvedExecDir, resolvedEntryPrefix) ||
|
|
54
|
+
prefixMatches(rawExecDir, rawEntryPrefix);
|
|
55
|
+
if (underExecDir)
|
|
56
|
+
return 'npm_global';
|
|
57
|
+
return 'npm_local';
|
|
8
58
|
}
|
|
@@ -153,6 +153,7 @@ export class ModuleLifecycle {
|
|
|
153
153
|
'Run `editmamei activate <key>` to restore Pro.');
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
|
+
let outcome = null;
|
|
156
157
|
try {
|
|
157
158
|
const prov = await provisionModules(license.key, {
|
|
158
159
|
force: reason === 'corrupt',
|
|
@@ -165,33 +166,44 @@ export class ModuleLifecycle {
|
|
|
165
166
|
for (const m of prov.installed) {
|
|
166
167
|
this.deps.logger.info(`Pro module updated to v${m.version}, restart to load.`);
|
|
167
168
|
}
|
|
168
|
-
|
|
169
|
+
outcome = 'updated';
|
|
169
170
|
}
|
|
170
|
-
if (prov.notConfigured) {
|
|
171
|
+
else if (prov.notConfigured) {
|
|
171
172
|
this.deps.logger.warn('Module delivery is not configured — staying Community.');
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const abiTooNew = prov.errors.some((e) => e.code === 'abi_too_new');
|
|
175
|
-
for (const e of prov.errors) {
|
|
176
|
-
if (e.code === 'abi_too_new')
|
|
177
|
-
continue;
|
|
178
|
-
this.deps.logger.warn(`Could not re-provision the ${e.sku} module (staying Community): ${e.message}`);
|
|
179
173
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
174
|
+
else {
|
|
175
|
+
const abiTooNew = prov.errors.some((e) => e.code === 'abi_too_new');
|
|
176
|
+
for (const e of prov.errors) {
|
|
177
|
+
if (e.code === 'abi_too_new')
|
|
178
|
+
continue;
|
|
179
|
+
this.deps.logger.warn(`Could not re-provision the ${e.sku} module (staying Community): ${e.message}`);
|
|
185
180
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
'
|
|
181
|
+
if (prov.errors.length === 0 || abiTooNew) {
|
|
182
|
+
if (reason === 'incompatible' || abiTooNew) {
|
|
183
|
+
this.deps.logger.warn('The published Pro module does not yet support this host version — staying ' +
|
|
184
|
+
'Community. Update Editmamei when a compatible release ships; ' +
|
|
185
|
+
'`editmamei report` files a diagnostic.');
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
this.deps.logger.warn('Re-provision installed nothing for the corrupt Pro module — staying ' +
|
|
189
|
+
'Community. Try `editmamei repair`; `editmamei report` if it persists.');
|
|
190
|
+
}
|
|
189
191
|
}
|
|
192
|
+
if (prov.errors.length > 0)
|
|
193
|
+
outcome = 'failed';
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
196
|
catch (err) {
|
|
193
197
|
this.deps.logger.warn(`Background Pro-module re-provision failed (staying Community): ` +
|
|
194
198
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
199
|
+
outcome = 'failed';
|
|
200
|
+
}
|
|
201
|
+
if (outcome !== null) {
|
|
202
|
+
try {
|
|
203
|
+
this.deps.onModuleUpdate?.(outcome);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
}
|
|
195
207
|
}
|
|
196
208
|
}
|
|
197
209
|
async ensureEntitledModuleFresh(delivery = {}) {
|
|
@@ -205,6 +217,7 @@ export class ModuleLifecycle {
|
|
|
205
217
|
const license = readLicense();
|
|
206
218
|
if (!license)
|
|
207
219
|
return;
|
|
220
|
+
let outcome = null;
|
|
208
221
|
try {
|
|
209
222
|
const prov = await provisionModules(license.key, {
|
|
210
223
|
config: delivery.config,
|
|
@@ -215,6 +228,12 @@ export class ModuleLifecycle {
|
|
|
215
228
|
for (const m of prov.installed) {
|
|
216
229
|
this.deps.logger.info(`Pro module updated to v${m.version} — restart to load.`);
|
|
217
230
|
}
|
|
231
|
+
if (prov.installed.length > 0) {
|
|
232
|
+
outcome = 'updated';
|
|
233
|
+
}
|
|
234
|
+
else if (prov.errors.length > 0) {
|
|
235
|
+
outcome = 'failed';
|
|
236
|
+
}
|
|
218
237
|
for (const e of prov.errors) {
|
|
219
238
|
if (e.code === 'abi_too_new') {
|
|
220
239
|
this.deps.logger.info(`A newer Pro module is published but needs a newer Editmamei than this ` +
|
|
@@ -228,6 +247,14 @@ export class ModuleLifecycle {
|
|
|
228
247
|
catch (err) {
|
|
229
248
|
this.deps.logger.warn(`Background Pro-module freshness check failed (staying on the installed version): ` +
|
|
230
249
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
250
|
+
outcome = 'failed';
|
|
251
|
+
}
|
|
252
|
+
if (outcome !== null) {
|
|
253
|
+
try {
|
|
254
|
+
this.deps.onModuleUpdate?.(outcome);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
}
|
|
231
258
|
}
|
|
232
259
|
}
|
|
233
260
|
}
|
|
Binary file
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
2
|
+
'ps_sequence',
|
|
3
|
+
'ps_batch',
|
|
4
|
+
'ps_ping',
|
|
5
|
+
'ps_overview',
|
|
6
|
+
'ps_list_capabilities',
|
|
7
|
+
'ps_get_preview',
|
|
8
|
+
'ps_get_selection_preview',
|
|
9
|
+
'ps_get_histogram',
|
|
10
|
+
'ps_inspect',
|
|
11
|
+
'ps_read_scene',
|
|
12
|
+
'ps_compare_regions',
|
|
13
|
+
'ps_get_layer_bounds_diff',
|
|
14
|
+
'ps_template_list',
|
|
15
|
+
'ps_template_recall',
|
|
16
|
+
'ps_list_actions',
|
|
17
|
+
'ps_report_problem',
|
|
18
|
+
'ps_detect',
|
|
19
|
+
'ps_detect_landmarks',
|
|
20
|
+
'ps_document',
|
|
21
|
+
'ps_template_verify',
|
|
22
|
+
'ps_resolve_placement',
|
|
23
|
+
'ps_template_save',
|
|
24
|
+
'ps_template_delete',
|
|
25
|
+
'ps_template_create_evidence',
|
|
26
|
+
]);
|
|
27
|
+
export const KEPT_WORK_TOOLS = new Set(['ps_export', 'ps_save_psd']);
|
|
28
|
+
export const MUTATING_TOOLS = new Set([
|
|
29
|
+
'ps_play_action',
|
|
30
|
+
'ps_execute_script',
|
|
31
|
+
'ps_add_adjustment_layer',
|
|
32
|
+
'ps_apply_adjustment',
|
|
33
|
+
'ps_create_document',
|
|
34
|
+
'ps_close_document',
|
|
35
|
+
'ps_open_document',
|
|
36
|
+
'ps_develop_raw',
|
|
37
|
+
'ps_filter',
|
|
38
|
+
'ps_group',
|
|
39
|
+
'ps_clipping_mask',
|
|
40
|
+
'ps_undo',
|
|
41
|
+
'ps_redo',
|
|
42
|
+
'ps_place_image',
|
|
43
|
+
'ps_resize_image',
|
|
44
|
+
'ps_crop_document',
|
|
45
|
+
'ps_convert_image_mode',
|
|
46
|
+
'ps_move_layer_to_position',
|
|
47
|
+
'ps_convert_to_smart_object',
|
|
48
|
+
'ps_rasterize_layer',
|
|
49
|
+
'ps_set_layer',
|
|
50
|
+
'ps_duplicate_layer',
|
|
51
|
+
'ps_copy_to_new_layer',
|
|
52
|
+
'ps_merge',
|
|
53
|
+
'ps_bake_layer',
|
|
54
|
+
'ps_add_layer_style',
|
|
55
|
+
'ps_create_layer',
|
|
56
|
+
'ps_delete_layer',
|
|
57
|
+
'ps_fill_layer',
|
|
58
|
+
'ps_add_fill_layer',
|
|
59
|
+
'ps_select_layer',
|
|
60
|
+
'ps_transform_layer',
|
|
61
|
+
'ps_warp_layer',
|
|
62
|
+
'ps_warp_layer_mesh',
|
|
63
|
+
'ps_warp_layer_along',
|
|
64
|
+
'ps_warp_layer_region',
|
|
65
|
+
'ps_warp_layer_to',
|
|
66
|
+
'ps_apply_camera_raw',
|
|
67
|
+
'ps_transform_canvas',
|
|
68
|
+
'ps_guides',
|
|
69
|
+
'ps_retouch',
|
|
70
|
+
'ps_apply_brush_stroke',
|
|
71
|
+
'ps_edit_object',
|
|
72
|
+
'ps_portrait_touchup',
|
|
73
|
+
'ps_add_text_to_object',
|
|
74
|
+
'ps_select_face_feature',
|
|
75
|
+
'ps_stroke_face_contour',
|
|
76
|
+
'ps_select_by_reference',
|
|
77
|
+
'ps_select',
|
|
78
|
+
'ps_select_subject',
|
|
79
|
+
'ps_select_sky',
|
|
80
|
+
'ps_select_subject_instance',
|
|
81
|
+
'ps_select_object',
|
|
82
|
+
'ps_replace_sky',
|
|
83
|
+
'ps_modify_selection',
|
|
84
|
+
'ps_selection_channel',
|
|
85
|
+
'ps_layer_mask',
|
|
86
|
+
'ps_path',
|
|
87
|
+
'ps_vector_mask',
|
|
88
|
+
'ps_apply_image',
|
|
89
|
+
'ps_calculations',
|
|
90
|
+
'ps_shape',
|
|
91
|
+
'ps_template_apply',
|
|
92
|
+
'ps_text',
|
|
93
|
+
]);
|
|
94
|
+
export function mapClientName(name) {
|
|
95
|
+
if (name === undefined)
|
|
96
|
+
return 'other';
|
|
97
|
+
const n = name.toLowerCase();
|
|
98
|
+
if (n.includes('claude-code') || n.includes('claude_code') || n.includes('claudecode')) {
|
|
99
|
+
return 'claude_code';
|
|
100
|
+
}
|
|
101
|
+
if (n.includes('claude'))
|
|
102
|
+
return 'claude_desktop';
|
|
103
|
+
if (n.includes('cursor'))
|
|
104
|
+
return 'cursor';
|
|
105
|
+
if (n.includes('windsurf'))
|
|
106
|
+
return 'windsurf';
|
|
107
|
+
if (n.includes('vscode') || n.includes('visual studio') || n.includes('code-oss')) {
|
|
108
|
+
return 'vscode';
|
|
109
|
+
}
|
|
110
|
+
return 'other';
|
|
111
|
+
}
|
|
112
|
+
export function parseMajor(version) {
|
|
113
|
+
if (version === undefined)
|
|
114
|
+
return null;
|
|
115
|
+
const m = /^(\d+)/.exec(version.trim());
|
|
116
|
+
return m ? Number(m[1]) : null;
|
|
117
|
+
}
|
|
118
|
+
export function boundMajor(n, max) {
|
|
119
|
+
return n !== null && Number.isFinite(n) && n >= 0 && n <= max ? n : null;
|
|
120
|
+
}
|
|
121
|
+
export function nodeMajor() {
|
|
122
|
+
return parseMajor(process.versions.node);
|
|
123
|
+
}
|
|
124
|
+
export function archToken() {
|
|
125
|
+
if (process.arch === 'x64')
|
|
126
|
+
return 'x64';
|
|
127
|
+
if (process.arch === 'arm64')
|
|
128
|
+
return 'arm64';
|
|
129
|
+
return 'other';
|
|
130
|
+
}
|
|
131
|
+
export function osMajor(platform, release) {
|
|
132
|
+
if (platform === 'win32') {
|
|
133
|
+
const parts = release.split('.');
|
|
134
|
+
const major = Number(parts[0]);
|
|
135
|
+
if (!Number.isFinite(major))
|
|
136
|
+
return null;
|
|
137
|
+
const build = Number(parts[2]);
|
|
138
|
+
return Number.isFinite(build) && build >= 22000 ? 11 : major;
|
|
139
|
+
}
|
|
140
|
+
if (platform === 'darwin') {
|
|
141
|
+
const m = /^(\d+)/.exec(release);
|
|
142
|
+
if (!m)
|
|
143
|
+
return null;
|
|
144
|
+
const darwinMajor = Number(m[1]);
|
|
145
|
+
return darwinMajor <= 24 ? darwinMajor - 9 : darwinMajor + 1;
|
|
146
|
+
}
|
|
147
|
+
const m = /^(\d+)/.exec(release);
|
|
148
|
+
return m ? Number(m[1]) : null;
|
|
149
|
+
}
|