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 CHANGED
@@ -9,7 +9,7 @@
9
9
  [![License](https://img.shields.io/badge/License-FSL--1.1--MIT-blue.svg)](LICENSE.md)
10
10
  [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS-lightgrey.svg)](docs/installation.md)
11
11
 
12
- 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.
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
- **82 tools across 16 capability groups** (62 Community, 20 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.
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 anonymous usage telemetry
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 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](docs/privacy.md).
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
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 anonymous usage telemetry
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:
@@ -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 { checkForUpdate, shouldCheckForUpdate } from '../update/check.js';
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
- const FIRST_RUN_DISCLOSURE = 'First run: Editmamei collects anonymous, content-free usage telemetry (tool name, ' +
46
- 'success, duration, version/edition/OS/PS-version, install channel) to find what breaks. ' +
47
- 'It never sends image content, file paths, or personal data. Opt out anytime: ' +
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
- constructor() {
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
- if (shouldCheckForUpdate(effectiveSettings.update_check)) {
88
- this.updateCheck = checkForUpdate().then((info) => {
89
- this.updateInfo = info;
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
- void this.sessionLog.append({
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: [{ type: 'text', text: 'Photoshop did not respond' + update.note }],
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: {
@@ -117,6 +117,7 @@ export const TOOL_GROUPS = {
117
117
  ps_apply_adjustment: 'adjust',
118
118
  ps_filter: 'filter',
119
119
  ps_apply_camera_raw: 'filter',
120
+ ps_develop_raw: 'filter',
120
121
  ps_retouch: 'retouch',
121
122
  ps_apply_brush_stroke: 'retouch',
122
123
  ps_create_layer: 'layers',
@@ -45,6 +45,7 @@ export const TOOL_TIERS = {
45
45
  ps_warp_layer_region: 'pro',
46
46
  ps_warp_layer_to: 'pro',
47
47
  ps_apply_camera_raw: 'pro',
48
+ ps_develop_raw: 'pro',
48
49
  ps_transform_canvas: 'community',
49
50
  ps_guides: 'community',
50
51
  ps_inspect: 'community',
@@ -1,8 +1,58 @@
1
+ import { realpathSync } from 'node:fs';
1
2
  import { EDITION } from './edition.js';
2
- export function resolveInstallChannel(env = process.env, edition = EDITION) {
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
- return 'npm';
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
  }