editmamei 1.4.0 → 1.5.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 +7 -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 +157 -19
- 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 +113 -26
- package/dist/license/advisory.js +109 -0
- package/dist/license/entitlement.js +34 -6
- package/dist/license/polar-client.js +50 -7
- package/dist/license/store.js +59 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/activity.js +149 -0
- package/dist/telemetry/client.js +170 -22
- package/dist/telemetry/events.js +48 -1
- package/dist/telemetry/outbox.js +43 -18
- 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
|
|
|
@@ -150,6 +150,7 @@ The source in this repository is the same code published to npm, so none of the
|
|
|
150
150
|
- **Install:** [docs/installation.md](docs/installation.md)
|
|
151
151
|
- **Getting started:** [docs/getting-started.md](docs/getting-started.md)
|
|
152
152
|
- **FAQ:** [docs/faq.md](docs/faq.md)
|
|
153
|
+
- **Troubleshooting:** [docs/troubleshooting.md](docs/troubleshooting.md)
|
|
153
154
|
- **Pro features:** [docs/pro-features.md](docs/pro-features.md)
|
|
154
155
|
- **Roadmap:** [docs/roadmap.md](docs/roadmap.md)
|
|
155
156
|
- **Bugs and feature requests:** [the issue tracker](https://github.com/editmamei/editmamei/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.
|
|
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,16 +8,18 @@ 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';
|
|
22
|
+
import { licenseAdvisory } from '../license/advisory.js';
|
|
21
23
|
import { runScript } from '../utils/run-script.js';
|
|
22
24
|
import { listTemplates } from '../utils/template-storage.js';
|
|
23
25
|
import { toolErrorResult } from '../utils/tool-helpers.js';
|
|
@@ -42,9 +44,20 @@ function warnLogScriptOnErrorOnce(logger) {
|
|
|
42
44
|
export function __resetLogScriptOnErrorWarnForTests() {
|
|
43
45
|
logScriptOnErrorWarned = false;
|
|
44
46
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
function hashRetryKey(tool, args) {
|
|
48
|
+
try {
|
|
49
|
+
const json = JSON.stringify({ tool, args });
|
|
50
|
+
return createHash('sha256').update(json).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const FIRST_RUN_DISCLOSURE = 'First run: Editmamei collects content-free usage telemetry (tool name, ' +
|
|
57
|
+
'success, duration, bytes returned, version/edition/OS/PS-version, install channel, ' +
|
|
58
|
+
'which AI client connected, Node/OS/architecture versions, and per-session counts like ' +
|
|
59
|
+
'edits made and retries) to find what breaks. It never sends image content, file paths, ' +
|
|
60
|
+
'or tool arguments. The only identifier is a random install ID. Opt out anytime: ' +
|
|
48
61
|
'`editmamei config set telemetry.usage false` (or edit ~/.editmamei/settings.json). ' +
|
|
49
62
|
'Opt in to sanitized diagnostics: `editmamei config set telemetry.diagnostics true`.';
|
|
50
63
|
export const BACKGROUND_VERSION_PROBE_TIMEOUT_MS = 3_000;
|
|
@@ -62,13 +75,22 @@ export class EditmameiServer {
|
|
|
62
75
|
}
|
|
63
76
|
psVersion = null;
|
|
64
77
|
lastPingReachedPs = null;
|
|
78
|
+
lastTelemetryCallKeyHash = null;
|
|
79
|
+
lastPsLocale = null;
|
|
80
|
+
lastDocDepth = null;
|
|
81
|
+
lastDocMode = null;
|
|
65
82
|
liveVersionResolutionAttempted = false;
|
|
66
83
|
updateInfo = null;
|
|
67
84
|
updateCheck = null;
|
|
68
85
|
updateNoticeShown = false;
|
|
69
86
|
snippetClient = new GoSnippetClient();
|
|
70
87
|
refreshLicenseOnPing = createPingLicenseRefresher();
|
|
71
|
-
|
|
88
|
+
moduleUpdateFailed = false;
|
|
89
|
+
licenseAdvisorySource;
|
|
90
|
+
licenseStoreOptions;
|
|
91
|
+
licenseAdvisoryShown = false;
|
|
92
|
+
constructor(opts = {}) {
|
|
93
|
+
this.licenseStoreOptions = opts.licenseStore ?? {};
|
|
72
94
|
this.logger = new Logger('EditmameiServer');
|
|
73
95
|
this.session = new Session();
|
|
74
96
|
this.sessionLog = new SessionLog(this.session.getSessionId());
|
|
@@ -84,10 +106,18 @@ export class EditmameiServer {
|
|
|
84
106
|
});
|
|
85
107
|
if (created)
|
|
86
108
|
this.logger.info(FIRST_RUN_DISCLOSURE);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
const shouldCheckForUpdateFn = opts.shouldCheckForUpdate ?? shouldCheckForUpdate;
|
|
110
|
+
const checkForUpdateWithStatusFn = opts.checkForUpdateWithStatus ?? checkForUpdateWithStatus;
|
|
111
|
+
if (shouldCheckForUpdateFn(effectiveSettings.update_check)) {
|
|
112
|
+
this.updateCheck = checkForUpdateWithStatusFn()
|
|
113
|
+
.then((result) => {
|
|
114
|
+
this.updateInfo = result.info;
|
|
115
|
+
if (result.status === 'newer')
|
|
116
|
+
this.telemetry.setBehindLatest(true);
|
|
117
|
+
else if (result.status === 'current')
|
|
118
|
+
this.telemetry.setBehindLatest(false);
|
|
119
|
+
})
|
|
120
|
+
.catch(() => undefined);
|
|
91
121
|
}
|
|
92
122
|
warnLogScriptOnErrorOnce(this.logger);
|
|
93
123
|
this.toolRegistry = new ToolRegistry({
|
|
@@ -97,22 +127,33 @@ export class EditmameiServer {
|
|
|
97
127
|
this.resolveLiveVersionInBackground();
|
|
98
128
|
}
|
|
99
129
|
}
|
|
100
|
-
|
|
130
|
+
const resultBytes = computeResultBytes(entry.result);
|
|
131
|
+
void this.sessionLog
|
|
132
|
+
.append({
|
|
101
133
|
tool: entry.tool,
|
|
102
134
|
args: entry.args,
|
|
103
135
|
success: entry.success,
|
|
104
136
|
duration_ms: entry.duration_ms,
|
|
105
137
|
...(entry.error ? { error: entry.error } : {}),
|
|
106
|
-
}, entry.result)
|
|
138
|
+
}, entry.result, resultBytes)
|
|
139
|
+
.catch((err) => {
|
|
140
|
+
this.logger.debug(`session log append failed: ${String(err)}`);
|
|
141
|
+
});
|
|
107
142
|
const telemetrySuccess = entry.tool === 'ps_ping' && this.lastPingReachedPs === false ? false : entry.success;
|
|
108
143
|
const isPingDowngrade = entry.tool === 'ps_ping' && !telemetrySuccess && entry.error === undefined;
|
|
109
144
|
const errorClass = classifyError(entry.error) ??
|
|
110
145
|
(telemetrySuccess ? null : isPingDowngrade ? 'ps_not_running' : NO_ERROR_TEXT_CLASS);
|
|
146
|
+
const telemetryCallKeyHash = hashRetryKey(entry.tool, entry.args);
|
|
147
|
+
const isTelemetryRetry = telemetryCallKeyHash !== null && telemetryCallKeyHash === this.lastTelemetryCallKeyHash;
|
|
148
|
+
if (telemetryCallKeyHash !== null)
|
|
149
|
+
this.lastTelemetryCallKeyHash = telemetryCallKeyHash;
|
|
111
150
|
this.telemetry.recordCall({
|
|
112
151
|
tool: entry.tool,
|
|
113
152
|
success: telemetrySuccess,
|
|
114
153
|
duration_ms: entry.duration_ms,
|
|
115
154
|
error_class: errorClass,
|
|
155
|
+
result_bytes: resultBytes,
|
|
156
|
+
retry: isTelemetryRetry,
|
|
116
157
|
});
|
|
117
158
|
if (!telemetrySuccess) {
|
|
118
159
|
this.telemetry.recordDiagnostic({
|
|
@@ -122,6 +163,9 @@ export class EditmameiServer {
|
|
|
122
163
|
(isPingDowngrade
|
|
123
164
|
? 'ps_ping did not reach Photoshop'
|
|
124
165
|
: 'tool reported failure with no message'),
|
|
166
|
+
...(this.lastPsLocale !== null ? { ps_locale: this.lastPsLocale } : {}),
|
|
167
|
+
...(this.lastDocDepth !== null ? { doc_depth: this.lastDocDepth } : {}),
|
|
168
|
+
...(this.lastDocMode !== null ? { doc_mode: this.lastDocMode } : {}),
|
|
125
169
|
});
|
|
126
170
|
}
|
|
127
171
|
},
|
|
@@ -136,9 +180,44 @@ export class EditmameiServer {
|
|
|
136
180
|
});
|
|
137
181
|
const srv = this.server;
|
|
138
182
|
this.sessionLog.setMcpClientGetter(() => srv.getClientVersion?.() ?? null);
|
|
183
|
+
let clientConnectedSent = false;
|
|
184
|
+
this.server.oninitialized = () => {
|
|
185
|
+
if (clientConnectedSent)
|
|
186
|
+
return;
|
|
187
|
+
clientConnectedSent = true;
|
|
188
|
+
const clientInfo = this.server.getClientVersion();
|
|
189
|
+
const caps = this.server.getClientCapabilities();
|
|
190
|
+
this.telemetry.recordClientConnected({
|
|
191
|
+
clientName: clientInfo?.name,
|
|
192
|
+
clientVersion: clientInfo?.version,
|
|
193
|
+
capSampling: caps?.sampling != null,
|
|
194
|
+
capElicitation: caps?.elicitation != null,
|
|
195
|
+
capRoots: caps?.roots != null,
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
this.licenseAdvisorySource =
|
|
199
|
+
opts.licenseAdvisory ??
|
|
200
|
+
((advisoryOpts) => {
|
|
201
|
+
if (process.env.VITEST !== undefined || process.env.NODE_ENV === 'test')
|
|
202
|
+
return null;
|
|
203
|
+
return licenseAdvisory(advisoryOpts);
|
|
204
|
+
});
|
|
139
205
|
this.registerTools();
|
|
140
206
|
this.setupHandlers();
|
|
141
207
|
}
|
|
208
|
+
buildLicenseAdvisoryNote() {
|
|
209
|
+
if (this.licenseAdvisoryShown)
|
|
210
|
+
return { note: '', stamp: () => { } };
|
|
211
|
+
const text = this.licenseAdvisorySource({ moduleUpdateFailed: this.moduleUpdateFailed });
|
|
212
|
+
if (!text)
|
|
213
|
+
return { note: '', stamp: () => { } };
|
|
214
|
+
return {
|
|
215
|
+
note: ' ' + text,
|
|
216
|
+
stamp: () => {
|
|
217
|
+
this.licenseAdvisoryShown = true;
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
142
221
|
registerTools() {
|
|
143
222
|
this.toolRegistry.register('ps_ping', {
|
|
144
223
|
tool: {
|
|
@@ -221,8 +300,14 @@ export class EditmameiServer {
|
|
|
221
300
|
this.moduleLifecycle = new ModuleLifecycle({
|
|
222
301
|
toolRegistry: this.toolRegistry,
|
|
223
302
|
logger: this.logger,
|
|
303
|
+
licenseStore: this.licenseStoreOptions,
|
|
224
304
|
assertToolsClassified: () => this.assertToolsClassified(),
|
|
225
305
|
classifyTool: (name) => this.classifyTool(name),
|
|
306
|
+
onModuleUpdate: (outcome) => {
|
|
307
|
+
if (outcome === 'failed')
|
|
308
|
+
this.moduleUpdateFailed = true;
|
|
309
|
+
this.telemetry.setModuleUpdate(outcome);
|
|
310
|
+
},
|
|
226
311
|
});
|
|
227
312
|
const proModule = this.moduleLifecycle.resolveProModule();
|
|
228
313
|
this.kernel = new Kernel({
|
|
@@ -332,6 +417,11 @@ export class EditmameiServer {
|
|
|
332
417
|
clearPendingRawDevelop();
|
|
333
418
|
}
|
|
334
419
|
}
|
|
420
|
+
else if (name === 'ps_develop_raw') {
|
|
421
|
+
const sc = result.structuredContent;
|
|
422
|
+
if (sc?.opened === true)
|
|
423
|
+
clearPendingRawDevelop();
|
|
424
|
+
}
|
|
335
425
|
else if (name === 'ps_apply_camera_raw' || name === 'ps_close_document') {
|
|
336
426
|
clearPendingRawDevelop();
|
|
337
427
|
}
|
|
@@ -366,12 +456,22 @@ export class EditmameiServer {
|
|
|
366
456
|
if (this.updateCheck)
|
|
367
457
|
await this.updateCheck;
|
|
368
458
|
const update = await this.buildUpdateNotice();
|
|
459
|
+
const advisory = this.buildLicenseAdvisoryNote();
|
|
460
|
+
const withAdvisory = (text) => {
|
|
461
|
+
advisory.stamp();
|
|
462
|
+
return text + advisory.note;
|
|
463
|
+
};
|
|
369
464
|
const connection = this.session.getConnection();
|
|
370
465
|
let version = 'Unknown';
|
|
371
466
|
let actionSetsCount = 0;
|
|
467
|
+
let actionSetsObserved = false;
|
|
372
468
|
let openDocuments = [];
|
|
469
|
+
let psLocale = null;
|
|
470
|
+
let docDepth = null;
|
|
471
|
+
let docMode = null;
|
|
373
472
|
const degraded = [];
|
|
374
473
|
let versionIsLive = false;
|
|
474
|
+
let pingStateObserved = false;
|
|
375
475
|
try {
|
|
376
476
|
const detected = await connection.getVersion();
|
|
377
477
|
if (detected)
|
|
@@ -381,12 +481,14 @@ export class EditmameiServer {
|
|
|
381
481
|
}
|
|
382
482
|
if (connection.getPhotoshopInfo() !== null && !(await connection.isCurrentlyRunning())) {
|
|
383
483
|
this.lastPingReachedPs = false;
|
|
484
|
+
this.lastDocDepth = null;
|
|
485
|
+
this.lastDocMode = null;
|
|
384
486
|
return {
|
|
385
487
|
content: [
|
|
386
488
|
{
|
|
387
489
|
type: 'text',
|
|
388
|
-
text: 'Photoshop does not appear to be running. Start Photoshop, then call ps_ping again.' +
|
|
389
|
-
update.note,
|
|
490
|
+
text: withAdvisory('Photoshop does not appear to be running. Start Photoshop, then call ps_ping again.' +
|
|
491
|
+
update.note),
|
|
390
492
|
},
|
|
391
493
|
],
|
|
392
494
|
structuredContent: {
|
|
@@ -406,11 +508,13 @@ export class EditmameiServer {
|
|
|
406
508
|
if (!alive) {
|
|
407
509
|
const reason = unsupportedHostReason();
|
|
408
510
|
this.lastPingReachedPs = false;
|
|
511
|
+
this.lastDocDepth = null;
|
|
512
|
+
this.lastDocMode = null;
|
|
409
513
|
return {
|
|
410
514
|
content: [
|
|
411
515
|
{
|
|
412
516
|
type: 'text',
|
|
413
|
-
text: (reason ?? 'Photoshop did not respond') + update.note,
|
|
517
|
+
text: withAdvisory((reason ?? 'Photoshop did not respond') + update.note),
|
|
414
518
|
},
|
|
415
519
|
],
|
|
416
520
|
structuredContent: {
|
|
@@ -430,8 +534,15 @@ export class EditmameiServer {
|
|
|
430
534
|
catch (err) {
|
|
431
535
|
this.logger.warn(`pingState snippet failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
432
536
|
this.lastPingReachedPs = false;
|
|
537
|
+
this.lastDocDepth = null;
|
|
538
|
+
this.lastDocMode = null;
|
|
433
539
|
return {
|
|
434
|
-
content: [
|
|
540
|
+
content: [
|
|
541
|
+
{
|
|
542
|
+
type: 'text',
|
|
543
|
+
text: withAdvisory('Photoshop did not respond' + update.note),
|
|
544
|
+
},
|
|
545
|
+
],
|
|
435
546
|
structuredContent: {
|
|
436
547
|
connected: false,
|
|
437
548
|
update_available: this.updateInfo,
|
|
@@ -439,19 +550,36 @@ export class EditmameiServer {
|
|
|
439
550
|
},
|
|
440
551
|
};
|
|
441
552
|
}
|
|
553
|
+
pingStateObserved = true;
|
|
442
554
|
if (state.version) {
|
|
443
555
|
version = state.version;
|
|
444
556
|
versionIsLive = true;
|
|
445
557
|
}
|
|
446
|
-
if (typeof state.action_sets_count === 'number')
|
|
558
|
+
if (typeof state.action_sets_count === 'number') {
|
|
447
559
|
actionSetsCount = state.action_sets_count;
|
|
560
|
+
actionSetsObserved = true;
|
|
561
|
+
}
|
|
448
562
|
if (Array.isArray(state.open_documents))
|
|
449
563
|
openDocuments = state.open_documents;
|
|
564
|
+
if (typeof state.locale === 'string')
|
|
565
|
+
psLocale = state.locale;
|
|
566
|
+
if (state.doc_depth === 8 || state.doc_depth === 16 || state.doc_depth === 32) {
|
|
567
|
+
docDepth = state.doc_depth;
|
|
568
|
+
}
|
|
569
|
+
if (state.doc_mode === 'rgb' ||
|
|
570
|
+
state.doc_mode === 'cmyk' ||
|
|
571
|
+
state.doc_mode === 'lab' ||
|
|
572
|
+
state.doc_mode === 'grayscale' ||
|
|
573
|
+
state.doc_mode === 'other') {
|
|
574
|
+
docMode = state.doc_mode;
|
|
575
|
+
}
|
|
450
576
|
}
|
|
451
577
|
let userTemplates = 0;
|
|
578
|
+
let templatesObserved = false;
|
|
452
579
|
try {
|
|
453
580
|
const templates = await listTemplates();
|
|
454
581
|
userTemplates = templates.length;
|
|
582
|
+
templatesObserved = true;
|
|
455
583
|
}
|
|
456
584
|
catch (err) {
|
|
457
585
|
this.logger.warn(`listTemplates failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -464,17 +592,27 @@ export class EditmameiServer {
|
|
|
464
592
|
this.psVersion = version;
|
|
465
593
|
this.telemetry.onPsVersionResolved();
|
|
466
594
|
}
|
|
595
|
+
if (psLocale !== null)
|
|
596
|
+
this.lastPsLocale = psLocale;
|
|
597
|
+
if (pingStateObserved) {
|
|
598
|
+
this.lastDocDepth = docDepth;
|
|
599
|
+
this.lastDocMode = docMode;
|
|
600
|
+
}
|
|
601
|
+
this.telemetry.setInstallAssets({
|
|
602
|
+
...(templatesObserved ? { templates_saved: userTemplates } : {}),
|
|
603
|
+
...(actionSetsObserved ? { action_sets: actionSetsCount } : {}),
|
|
604
|
+
});
|
|
467
605
|
this.lastPingReachedPs = true;
|
|
468
606
|
const degradedNote = degraded.length ? ` (degraded: ${degraded.join(', ')})` : '';
|
|
469
607
|
return {
|
|
470
608
|
content: [
|
|
471
609
|
{
|
|
472
610
|
type: 'text',
|
|
473
|
-
text: `Connected to Photoshop (v${version}). ` +
|
|
611
|
+
text: withAdvisory(`Connected to Photoshop (v${version}). ` +
|
|
474
612
|
`${actionSetsCount} custom action set(s), ${userTemplates} saved template(s), ` +
|
|
475
613
|
`${openDocuments.length} open document(s)${openDocuments.length ? ': ' + openDocuments.join(', ') : ''}` +
|
|
476
614
|
`${degradedNote}.` +
|
|
477
|
-
update.note,
|
|
615
|
+
update.note),
|
|
478
616
|
},
|
|
479
617
|
],
|
|
480
618
|
structuredContent: {
|
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
|
}
|