editmamei 1.5.0 → 1.6.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.
Files changed (48) hide show
  1. package/README.md +17 -1
  2. package/dist/backends/detect-editors.js +79 -0
  3. package/dist/backends/gimp/backend.js +96 -0
  4. package/dist/backends/gimp/bridge/lib.py +1229 -0
  5. package/dist/backends/gimp/bridge/ops.py +1501 -0
  6. package/dist/backends/gimp/detect.js +86 -0
  7. package/dist/backends/gimp/errors.js +21 -0
  8. package/dist/backends/gimp/launcher.js +6 -0
  9. package/dist/backends/gimp/session.js +629 -0
  10. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  11. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  12. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  13. package/dist/cli/config.js +28 -0
  14. package/dist/core/server.js +142 -56
  15. package/dist/core/settings.js +25 -0
  16. package/dist/core/tool-activity.js +114 -0
  17. package/dist/core/tool-groups.js +16 -0
  18. package/dist/core/tool-tiers.js +16 -0
  19. package/dist/diagnostics/collect.js +4 -0
  20. package/dist/index.js +5 -1
  21. package/dist/kernel/kernel.js +3 -0
  22. package/dist/kernel/module-lifecycle.js +69 -9
  23. package/dist/license/advisory.js +109 -0
  24. package/dist/license/entitlement.js +34 -6
  25. package/dist/license/polar-client.js +50 -7
  26. package/dist/license/store.js +59 -0
  27. package/dist/modules/gimp/index.js +35 -0
  28. package/dist/skills/editmamei-skill.zip +0 -0
  29. package/dist/telemetry/activity.js +1 -93
  30. package/dist/telemetry/client.js +55 -17
  31. package/dist/telemetry/events.js +5 -0
  32. package/dist/telemetry/outbox.js +43 -18
  33. package/dist/tools/adjustment-tools.js +2 -1
  34. package/dist/tools/gimp-adjustment-tools.js +382 -0
  35. package/dist/tools/gimp-core-tools.js +288 -0
  36. package/dist/tools/gimp-document-tools.js +321 -0
  37. package/dist/tools/gimp-filter-tools.js +93 -0
  38. package/dist/tools/gimp-geometry-tools.js +191 -0
  39. package/dist/tools/gimp-inspect-tools.js +70 -0
  40. package/dist/tools/gimp-mask-tools.js +102 -0
  41. package/dist/tools/gimp-shared.js +51 -0
  42. package/dist/tools/gimp-verify-tools.js +374 -0
  43. package/dist/utils/gimp-path.js +27 -0
  44. package/dist/utils/operation-timeouts.js +16 -0
  45. package/dist/utils/session-log.js +19 -1
  46. package/dist/utils/tool-helpers.js +14 -0
  47. package/dist/version.js +1 -1
  48. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { Logger } from '../utils/logger.js';
5
5
  import { ToolRegistry } from './tool-registry.js';
6
6
  import { tierOf } from './tool-tiers.js';
7
7
  import { groupOf, GROUPS } from './tool-groups.js';
8
+ import { CAMERA_RAW_TOOL, RAW_DEVELOP_TOOL } from './tool-activity.js';
8
9
  import { EDITION } from '../edition.js';
9
10
  import { VERSION } from '../version.js';
10
11
  import { Session } from './session.js';
@@ -19,6 +20,7 @@ import { createHash } from 'node:crypto';
19
20
  import { GoSnippetClient, coreBinaryName } from '../api/snippet-client.js';
20
21
  import { isProEntitled } from '../license/entitlement.js';
21
22
  import { createPingLicenseRefresher } from '../license/ping-refresh.js';
23
+ import { licenseAdvisory } from '../license/advisory.js';
22
24
  import { runScript } from '../utils/run-script.js';
23
25
  import { listTemplates } from '../utils/template-storage.js';
24
26
  import { toolErrorResult } from '../utils/tool-helpers.js';
@@ -28,6 +30,9 @@ import { ceModule } from '../modules/ce/index.js';
28
30
  import { hostDetectionRuntime } from '../detection/runtime.js';
29
31
  import { ModuleLifecycle, classifyModuleOutcome } from '../kernel/module-lifecycle.js';
30
32
  import { unsupportedHostReason } from '../platform/host-platform.js';
33
+ import { gimpModule } from '../modules/gimp/index.js';
34
+ import { GimpBackend } from '../backends/gimp/backend.js';
35
+ import { createDiagnosticsTools } from '../tools/diagnostics-tools.js';
31
36
  export { classifyModuleOutcome };
32
37
  let logScriptOnErrorWarned = false;
33
38
  function warnLogScriptOnErrorOnce(logger) {
@@ -67,6 +72,9 @@ export class EditmameiServer {
67
72
  session;
68
73
  sessionLog;
69
74
  telemetry;
75
+ editorOpts;
76
+ gimpBackend;
77
+ lastLoggedGimpVersion;
70
78
  kernel;
71
79
  moduleLifecycle;
72
80
  get moduleSkipReason() {
@@ -84,7 +92,24 @@ export class EditmameiServer {
84
92
  updateNoticeShown = false;
85
93
  snippetClient = new GoSnippetClient();
86
94
  refreshLicenseOnPing = createPingLicenseRefresher();
95
+ moduleUpdateFailed = false;
96
+ licenseAdvisorySource;
97
+ licenseStoreOptions;
98
+ licenseAdvisoryShown = false;
87
99
  constructor(opts = {}) {
100
+ this.licenseStoreOptions = opts.licenseStore ?? {};
101
+ this.editorOpts = opts.editors ?? {
102
+ registerPhotoshop: true,
103
+ registerGimp: false,
104
+ gimpInstall: null,
105
+ gimpDetectionTimedOut: false,
106
+ };
107
+ if (this.editorOpts.registerGimp) {
108
+ this.gimpBackend = new GimpBackend(this.editorOpts.gimpInstall, {
109
+ gimpDetectionTimedOut: this.editorOpts.gimpDetectionTimedOut,
110
+ gimpPath: this.editorOpts.gimpPathOverride,
111
+ });
112
+ }
88
113
  this.logger = new Logger('EditmameiServer');
89
114
  this.session = new Session();
90
115
  this.sessionLog = new SessionLog(this.session.getSessionId());
@@ -121,6 +146,13 @@ export class EditmameiServer {
121
146
  this.resolveLiveVersionInBackground();
122
147
  }
123
148
  }
149
+ if (entry.success && entry.tool.startsWith('gimp_') && this.gimpBackend?.gimpVersion) {
150
+ const version = this.gimpBackend.gimpVersion;
151
+ if (version !== this.lastLoggedGimpVersion) {
152
+ this.lastLoggedGimpVersion = version;
153
+ void this.sessionLog.setGimpVersion(version);
154
+ }
155
+ }
124
156
  const resultBytes = computeResultBytes(entry.result);
125
157
  void this.sessionLog
126
158
  .append({
@@ -189,56 +221,77 @@ export class EditmameiServer {
189
221
  capRoots: caps?.roots != null,
190
222
  });
191
223
  };
224
+ this.licenseAdvisorySource =
225
+ opts.licenseAdvisory ??
226
+ ((advisoryOpts) => {
227
+ if (process.env.VITEST !== undefined || process.env.NODE_ENV === 'test')
228
+ return null;
229
+ return licenseAdvisory(advisoryOpts);
230
+ });
192
231
  this.registerTools();
193
232
  this.setupHandlers();
194
233
  }
234
+ buildLicenseAdvisoryNote() {
235
+ if (this.licenseAdvisoryShown)
236
+ return { note: '', stamp: () => { } };
237
+ const text = this.licenseAdvisorySource({ moduleUpdateFailed: this.moduleUpdateFailed });
238
+ if (!text)
239
+ return { note: '', stamp: () => { } };
240
+ return {
241
+ note: ' ' + text,
242
+ stamp: () => {
243
+ this.licenseAdvisoryShown = true;
244
+ },
245
+ };
246
+ }
195
247
  registerTools() {
196
- this.toolRegistry.register('ps_ping', {
197
- tool: {
198
- name: 'ps_ping',
199
- 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).',
200
- inputSchema: {
201
- type: 'object',
202
- properties: {},
203
- },
204
- outputSchema: {
205
- type: 'object',
206
- properties: {
207
- connected: { type: 'boolean' },
208
- version: { type: 'string' },
209
- custom_action_sets: { type: 'number' },
210
- user_templates: { type: 'number' },
211
- open_documents: { type: 'array', items: { type: 'string' } },
212
- degraded: {
213
- type: 'array',
214
- items: { type: 'string' },
215
- 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.',
216
- },
217
- update_available: {
218
- type: ['object', 'null'],
219
- description: 'Set when a newer Editmamei version is published, else null: { current, latest, channel (npm/mcpb/dev), how_to_update, fixed_tools (tools whose recorded failures the newer version fixes) }. The relay instruction rides the ping TEXT on the first ping — see notify_user. Anonymous npm-registry check at boot; opt out with `editmamei config set update_check false`.',
220
- properties: {
221
- current: { type: 'string' },
222
- latest: { type: 'string' },
223
- channel: { type: 'string' },
224
- how_to_update: { type: 'string' },
225
- fixed_tools: { type: 'array', items: { type: 'string' } },
248
+ if (this.editorOpts.registerPhotoshop)
249
+ this.toolRegistry.register('ps_ping', {
250
+ tool: {
251
+ name: 'ps_ping',
252
+ 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).',
253
+ inputSchema: {
254
+ type: 'object',
255
+ properties: {},
256
+ },
257
+ outputSchema: {
258
+ type: 'object',
259
+ properties: {
260
+ connected: { type: 'boolean' },
261
+ version: { type: 'string' },
262
+ custom_action_sets: { type: 'number' },
263
+ user_templates: { type: 'number' },
264
+ open_documents: { type: 'array', items: { type: 'string' } },
265
+ degraded: {
266
+ type: 'array',
267
+ items: { type: 'string' },
268
+ 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.',
269
+ },
270
+ update_available: {
271
+ type: ['object', 'null'],
272
+ description: 'Set when a newer Editmamei version is published, else null: { current, latest, channel (npm/mcpb/dev), how_to_update, fixed_tools (tools whose recorded failures the newer version fixes) }. The relay instruction rides the ping TEXT on the first ping — see notify_user. Anonymous npm-registry check at boot; opt out with `editmamei config set update_check false`.',
273
+ properties: {
274
+ current: { type: 'string' },
275
+ latest: { type: 'string' },
276
+ channel: { type: 'string' },
277
+ how_to_update: { type: 'string' },
278
+ fixed_tools: { type: 'array', items: { type: 'string' } },
279
+ },
280
+ },
281
+ notify_user: {
282
+ type: 'boolean',
283
+ description: 'True on the one ping whose text carries the update notice — relay that notice to the user before continuing. False on later pings and when no update is available.',
226
284
  },
227
- },
228
- notify_user: {
229
- type: 'boolean',
230
- description: 'True on the one ping whose text carries the update notice — relay that notice to the user before continuing. False on later pings and when no update is available.',
231
285
  },
232
286
  },
287
+ annotations: {
288
+ title: 'Ping Photoshop',
289
+ readOnlyHint: true,
290
+ idempotentHint: true,
291
+ },
233
292
  },
234
- annotations: {
235
- title: 'Ping Photoshop',
236
- readOnlyHint: true,
237
- idempotentHint: true,
238
- },
239
- },
240
- handler: async () => await this.pingPhotoshop(),
241
- });
293
+ handler: async () => await this.pingPhotoshop(),
294
+ });
242
295
  this.toolRegistry.register('ps_list_capabilities', {
243
296
  tool: {
244
297
  name: 'ps_list_capabilities',
@@ -274,9 +327,14 @@ export class EditmameiServer {
274
327
  this.moduleLifecycle = new ModuleLifecycle({
275
328
  toolRegistry: this.toolRegistry,
276
329
  logger: this.logger,
330
+ licenseStore: this.licenseStoreOptions,
277
331
  assertToolsClassified: () => this.assertToolsClassified(),
278
332
  classifyTool: (name) => this.classifyTool(name),
279
- onModuleUpdate: (outcome) => this.telemetry.setModuleUpdate(outcome),
333
+ onModuleUpdate: (outcome) => {
334
+ if (outcome === 'failed')
335
+ this.moduleUpdateFailed = true;
336
+ this.telemetry.setModuleUpdate(outcome);
337
+ },
280
338
  });
281
339
  const proModule = this.moduleLifecycle.resolveProModule();
282
340
  this.kernel = new Kernel({
@@ -284,6 +342,7 @@ export class EditmameiServer {
284
342
  connection: this.session.getConnection(),
285
343
  snippet: this.snippetClient,
286
344
  detection: hostDetectionRuntime(),
345
+ gimp: this.gimpBackend,
287
346
  resolveModuleSnippet: (manifest) => manifest.goCoreSnippets && manifest.goCoreSnippets.length > 0 && proModule
288
347
  ? new GoSnippetClient({ binaryPath: join(proModule.binDir, coreBinaryName()) })
289
348
  : null,
@@ -291,11 +350,22 @@ export class EditmameiServer {
291
350
  logger: this.logger,
292
351
  });
293
352
  this.moduleLifecycle.setKernel(this.kernel);
294
- this.kernel.loadBuiltins([ceModule]);
353
+ const builtins = [];
354
+ if (this.editorOpts.registerPhotoshop)
355
+ builtins.push(ceModule);
356
+ if (this.editorOpts.registerGimp)
357
+ builtins.push(gimpModule);
358
+ this.kernel.loadBuiltins(builtins);
295
359
  this.assertToolsClassified();
360
+ if (!this.editorOpts.registerPhotoshop) {
361
+ const diagnosticsDefs = createDiagnosticsTools(this.session.getConnection(), this.snippetClient).filter((def) => def.tool.name === 'ps_report_problem');
362
+ this.toolRegistry.registerAll(diagnosticsDefs);
363
+ }
296
364
  this.logger.info(`Registered ${this.toolRegistry.count()} tools (edition: ${EDITION})`);
297
365
  }
298
366
  async loadModules() {
367
+ if (!this.editorOpts.registerPhotoshop)
368
+ return;
299
369
  return this.moduleLifecycle.loadModules();
300
370
  }
301
371
  computeModuleStatus() {
@@ -378,7 +448,7 @@ export class EditmameiServer {
378
448
  return;
379
449
  if (name === 'ps_open_document') {
380
450
  const sc = result.structuredContent;
381
- if (sc?.is_raw_source === true && this.toolRegistry.get('ps_apply_camera_raw')) {
451
+ if (sc?.is_raw_source === true && this.toolRegistry.get(CAMERA_RAW_TOOL)) {
382
452
  const opened = sc;
383
453
  markRawOpened(String(opened.document_name ?? ''), String(opened.file_path ?? ''));
384
454
  }
@@ -386,12 +456,12 @@ export class EditmameiServer {
386
456
  clearPendingRawDevelop();
387
457
  }
388
458
  }
389
- else if (name === 'ps_develop_raw') {
459
+ else if (name === RAW_DEVELOP_TOOL) {
390
460
  const sc = result.structuredContent;
391
461
  if (sc?.opened === true)
392
462
  clearPendingRawDevelop();
393
463
  }
394
- else if (name === 'ps_apply_camera_raw' || name === 'ps_close_document') {
464
+ else if (name === CAMERA_RAW_TOOL || name === 'ps_close_document') {
395
465
  clearPendingRawDevelop();
396
466
  }
397
467
  }
@@ -425,6 +495,11 @@ export class EditmameiServer {
425
495
  if (this.updateCheck)
426
496
  await this.updateCheck;
427
497
  const update = await this.buildUpdateNotice();
498
+ const advisory = this.buildLicenseAdvisoryNote();
499
+ const withAdvisory = (text) => {
500
+ advisory.stamp();
501
+ return text + advisory.note;
502
+ };
428
503
  const connection = this.session.getConnection();
429
504
  let version = 'Unknown';
430
505
  let actionSetsCount = 0;
@@ -451,8 +526,8 @@ export class EditmameiServer {
451
526
  content: [
452
527
  {
453
528
  type: 'text',
454
- text: 'Photoshop does not appear to be running. Start Photoshop, then call ps_ping again.' +
455
- update.note,
529
+ text: withAdvisory('Photoshop does not appear to be running. Start Photoshop, then call ps_ping again.' +
530
+ update.note),
456
531
  },
457
532
  ],
458
533
  structuredContent: {
@@ -478,7 +553,7 @@ export class EditmameiServer {
478
553
  content: [
479
554
  {
480
555
  type: 'text',
481
- text: (reason ?? 'Photoshop did not respond') + update.note,
556
+ text: withAdvisory((reason ?? 'Photoshop did not respond') + update.note),
482
557
  },
483
558
  ],
484
559
  structuredContent: {
@@ -501,7 +576,12 @@ export class EditmameiServer {
501
576
  this.lastDocDepth = null;
502
577
  this.lastDocMode = null;
503
578
  return {
504
- content: [{ type: 'text', text: 'Photoshop did not respond' + update.note }],
579
+ content: [
580
+ {
581
+ type: 'text',
582
+ text: withAdvisory('Photoshop did not respond' + update.note),
583
+ },
584
+ ],
505
585
  structuredContent: {
506
586
  connected: false,
507
587
  update_available: this.updateInfo,
@@ -567,11 +647,11 @@ export class EditmameiServer {
567
647
  content: [
568
648
  {
569
649
  type: 'text',
570
- text: `Connected to Photoshop (v${version}). ` +
650
+ text: withAdvisory(`Connected to Photoshop (v${version}). ` +
571
651
  `${actionSetsCount} custom action set(s), ${userTemplates} saved template(s), ` +
572
652
  `${openDocuments.length} open document(s)${openDocuments.length ? ': ' + openDocuments.join(', ') : ''}` +
573
653
  `${degradedNote}.` +
574
- update.note,
654
+ update.note),
575
655
  },
576
656
  ],
577
657
  structuredContent: {
@@ -626,18 +706,24 @@ export class EditmameiServer {
626
706
  await this.loadModules();
627
707
  const transport = new StdioServerTransport();
628
708
  await this.server.connect(transport);
629
- void this.session.initialize().catch(() => undefined);
630
- void this.reprovisionIfModuleSkipped();
631
- void this.ensureEntitledModuleFresh();
709
+ if (this.editorOpts.registerPhotoshop) {
710
+ void this.session.initialize().catch(() => undefined);
711
+ void this.reprovisionIfModuleSkipped();
712
+ void this.ensureEntitledModuleFresh();
713
+ }
632
714
  this.telemetry.start();
633
715
  void this.telemetry.flushOutboxOnStartup();
634
716
  this.server.onclose = () => {
635
717
  void this.telemetry.shutdown();
718
+ if (this.gimpBackend)
719
+ void this.gimpBackend.shutdown();
636
720
  };
637
721
  this.logger.info('Editmamei is listening on stdio');
638
722
  }
639
723
  async stop() {
640
724
  await this.telemetry.shutdown();
725
+ if (this.gimpBackend)
726
+ await this.gimpBackend.shutdown();
641
727
  await this.session.disconnect();
642
728
  await this.sessionLog.close();
643
729
  this.logger.info('Editmamei has shut down');
@@ -3,6 +3,7 @@ import { join, dirname } from 'node:path';
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { Logger } from '../utils/logger.js';
6
+ import { requireAbsoluteGimpPath } from '../utils/gimp-path.js';
6
7
  const logger = new Logger('Settings');
7
8
  const SETTINGS_DIRNAME = '.editmamei';
8
9
  const SETTINGS_FILENAME = 'settings.json';
@@ -15,12 +16,15 @@ export function settingsPath(opts = {}) {
15
16
  export function mintInstallId() {
16
17
  return randomBytes(16).toString('hex');
17
18
  }
19
+ const EDITOR_PINS = ['auto', 'photoshop', 'gimp'];
18
20
  function defaults(installId) {
19
21
  return {
20
22
  telemetry: { usage: true, diagnostics: false, install_id: installId },
21
23
  privacy: { send_previews_to_llm: true },
22
24
  ps_path: null,
23
25
  update_check: true,
26
+ editor: 'auto',
27
+ gimp_path: null,
24
28
  };
25
29
  }
26
30
  function coerce(raw, installId) {
@@ -45,8 +49,23 @@ function coerce(raw, installId) {
45
49
  },
46
50
  ps_path: typeof r.ps_path === 'string' ? r.ps_path : null,
47
51
  update_check: typeof r.update_check === 'boolean' ? r.update_check : base.update_check,
52
+ editor: typeof r.editor === 'string' && EDITOR_PINS.includes(r.editor)
53
+ ? r.editor
54
+ : base.editor,
55
+ gimp_path: coerceGimpPath(r.gimp_path),
48
56
  };
49
57
  }
58
+ function coerceGimpPath(raw) {
59
+ if (typeof raw !== 'string' || raw.trim() === '')
60
+ return null;
61
+ try {
62
+ return requireAbsoluteGimpPath('gimp_path', raw);
63
+ }
64
+ catch (err) {
65
+ logger.warn(`ignoring settings.json gimp_path (falling back to auto-detect): ${errMsg(err)}`);
66
+ return null;
67
+ }
68
+ }
50
69
  export function loadSettings(opts = {}) {
51
70
  const path = settingsPath(opts);
52
71
  if (!existsSync(path)) {
@@ -110,6 +129,12 @@ export function applyUpdateCheckEnvOverride(settings, env = process.env) {
110
129
  return settings;
111
130
  return { ...settings, update_check: v };
112
131
  }
132
+ export function applyEditorEnvOverride(settings, env = process.env) {
133
+ const raw = env.EDITMAMEI_EDITOR?.trim().toLowerCase();
134
+ if (!raw || !EDITOR_PINS.includes(raw))
135
+ return settings;
136
+ return { ...settings, editor: raw };
137
+ }
113
138
  export function saveSettings(settings, opts = {}) {
114
139
  const path = settingsPath(opts);
115
140
  const dir = dirname(path);
@@ -0,0 +1,114 @@
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
+ 'gimp_ping',
27
+ 'gimp_overview',
28
+ 'gimp_inspect',
29
+ 'gimp_get_preview',
30
+ 'gimp_get_histogram',
31
+ 'gimp_compare',
32
+ ]);
33
+ export const KEPT_WORK_TOOLS = new Set([
34
+ 'ps_export',
35
+ 'ps_save_psd',
36
+ 'gimp_export',
37
+ 'gimp_save_xcf',
38
+ ]);
39
+ export const MUTATING_TOOLS = new Set([
40
+ 'ps_play_action',
41
+ 'ps_execute_script',
42
+ 'ps_add_adjustment_layer',
43
+ 'ps_apply_adjustment',
44
+ 'ps_create_document',
45
+ 'ps_close_document',
46
+ 'ps_open_document',
47
+ 'ps_develop_raw',
48
+ 'ps_filter',
49
+ 'ps_group',
50
+ 'ps_clipping_mask',
51
+ 'ps_undo',
52
+ 'ps_redo',
53
+ 'ps_place_image',
54
+ 'ps_resize_image',
55
+ 'ps_crop_document',
56
+ 'ps_convert_image_mode',
57
+ 'ps_move_layer_to_position',
58
+ 'ps_convert_to_smart_object',
59
+ 'ps_rasterize_layer',
60
+ 'ps_set_layer',
61
+ 'ps_duplicate_layer',
62
+ 'ps_copy_to_new_layer',
63
+ 'ps_merge',
64
+ 'ps_bake_layer',
65
+ 'ps_add_layer_style',
66
+ 'ps_create_layer',
67
+ 'ps_delete_layer',
68
+ 'ps_fill_layer',
69
+ 'ps_add_fill_layer',
70
+ 'ps_select_layer',
71
+ 'ps_transform_layer',
72
+ 'ps_warp_layer',
73
+ 'ps_warp_layer_mesh',
74
+ 'ps_warp_layer_along',
75
+ 'ps_warp_layer_region',
76
+ 'ps_warp_layer_to',
77
+ 'ps_apply_camera_raw',
78
+ 'ps_transform_canvas',
79
+ 'ps_guides',
80
+ 'ps_retouch',
81
+ 'ps_apply_brush_stroke',
82
+ 'ps_edit_object',
83
+ 'ps_portrait_touchup',
84
+ 'ps_add_text_to_object',
85
+ 'ps_select_face_feature',
86
+ 'ps_stroke_face_contour',
87
+ 'ps_select_by_reference',
88
+ 'ps_select',
89
+ 'ps_select_subject',
90
+ 'ps_select_sky',
91
+ 'ps_select_subject_instance',
92
+ 'ps_select_object',
93
+ 'ps_replace_sky',
94
+ 'ps_modify_selection',
95
+ 'ps_selection_channel',
96
+ 'ps_layer_mask',
97
+ 'ps_path',
98
+ 'ps_vector_mask',
99
+ 'ps_apply_image',
100
+ 'ps_calculations',
101
+ 'ps_shape',
102
+ 'ps_template_apply',
103
+ 'ps_text',
104
+ 'gimp_open_document',
105
+ 'gimp_close_document',
106
+ 'gimp_add_adjustment',
107
+ 'gimp_filter',
108
+ 'gimp_crop_document',
109
+ 'gimp_resize_image',
110
+ 'gimp_transform_canvas',
111
+ 'gimp_create_mask',
112
+ ]);
113
+ export const RAW_DEVELOP_TOOL = 'ps_develop_raw';
114
+ export const CAMERA_RAW_TOOL = 'ps_apply_camera_raw';
@@ -171,6 +171,22 @@ export const TOOL_GROUPS = {
171
171
  ps_play_action: 'automation',
172
172
  ps_execute_script: 'automation',
173
173
  ps_batch: 'automation',
174
+ gimp_ping: 'core',
175
+ gimp_overview: 'core',
176
+ gimp_inspect: 'inspect',
177
+ gimp_get_preview: 'verify',
178
+ gimp_get_histogram: 'verify',
179
+ gimp_compare: 'verify',
180
+ gimp_open_document: 'document',
181
+ gimp_close_document: 'document',
182
+ gimp_save_xcf: 'document',
183
+ gimp_export: 'document',
184
+ gimp_crop_document: 'document',
185
+ gimp_resize_image: 'document',
186
+ gimp_transform_canvas: 'document',
187
+ gimp_add_adjustment: 'adjust',
188
+ gimp_filter: 'filter',
189
+ gimp_create_mask: 'masks',
174
190
  };
175
191
  export function groupOf(toolName) {
176
192
  const group = TOOL_GROUPS[toolName];
@@ -89,6 +89,22 @@ export const TOOL_TIERS = {
89
89
  ps_template_recall: 'pro',
90
90
  ps_template_delete: 'pro',
91
91
  ps_text: 'community',
92
+ gimp_ping: 'community',
93
+ gimp_overview: 'community',
94
+ gimp_open_document: 'community',
95
+ gimp_close_document: 'community',
96
+ gimp_save_xcf: 'community',
97
+ gimp_export: 'community',
98
+ gimp_inspect: 'community',
99
+ gimp_add_adjustment: 'community',
100
+ gimp_filter: 'community',
101
+ gimp_crop_document: 'community',
102
+ gimp_resize_image: 'community',
103
+ gimp_transform_canvas: 'community',
104
+ gimp_create_mask: 'community',
105
+ gimp_get_preview: 'community',
106
+ gimp_get_histogram: 'community',
107
+ gimp_compare: 'community',
92
108
  };
93
109
  export function tierOf(toolName) {
94
110
  const tier = TOOL_TIERS[toolName];
@@ -35,6 +35,7 @@ export async function collectDiagnostics(opts = {}) {
35
35
  const recentIds = await listRecentSessionIds(MAX_RECENT_SESSIONS, { dir: sessionsDir });
36
36
  const recent_sessions = [];
37
37
  let psVersion = opts.psVersion ?? null;
38
+ let gimpVersion = null;
38
39
  let mcpClient = null;
39
40
  for (const id of recentIds) {
40
41
  const entries = await readSessionLog(id, { dir: sessionsDir });
@@ -51,6 +52,8 @@ export async function collectDiagnostics(opts = {}) {
51
52
  if (meta) {
52
53
  if (!psVersion && meta.ps_version)
53
54
  psVersion = meta.ps_version;
55
+ if (!gimpVersion && meta.gimp_version)
56
+ gimpVersion = meta.gimp_version;
54
57
  if (!mcpClient)
55
58
  mcpClient = mcpClientLabel(meta);
56
59
  }
@@ -78,6 +81,7 @@ export async function collectDiagnostics(opts = {}) {
78
81
  node_version: process.version,
79
82
  install_id: settings.telemetry.install_id,
80
83
  ps_version: psVersion,
84
+ gimp_version: gimpVersion,
81
85
  mcp_client: mcpClient,
82
86
  settings: {
83
87
  telemetry_usage: settings.telemetry.usage,
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import { routeCli } from './cli/router.js';
6
6
  import { maybeActivateFromEnv } from './license/env-activation.js';
7
7
  import { refreshIfStale } from './license/entitlement.js';
8
8
  import { Logger } from './utils/logger.js';
9
+ import { resolveBootEditors } from './backends/detect-editors.js';
10
+ import { loadSettings } from './core/settings.js';
9
11
  const logger = new Logger('Main');
10
12
  function installProcessHandlers() {
11
13
  process.on('unhandledRejection', (reason, promise) => {
@@ -39,7 +41,9 @@ async function main() {
39
41
  logger.info('Starting Editmamei...');
40
42
  await maybeActivateFromEnv();
41
43
  await refreshIfStale();
42
- const server = new EditmameiServer();
44
+ const { settings } = loadSettings();
45
+ const editors = await resolveBootEditors({ settings });
46
+ const server = new EditmameiServer({ editors });
43
47
  await server.start();
44
48
  let shuttingDown = false;
45
49
  const shutdown = async (signal) => {
@@ -9,6 +9,7 @@ export class Kernel {
9
9
  snippet;
10
10
  resolveModuleSnippet;
11
11
  detection;
12
+ gimp;
12
13
  sessionId;
13
14
  logger;
14
15
  invokeDepth = new AsyncLocalStorage();
@@ -19,6 +20,7 @@ export class Kernel {
19
20
  this.snippet = deps.snippet;
20
21
  this.resolveModuleSnippet = deps.resolveModuleSnippet;
21
22
  this.detection = deps.detection;
23
+ this.gimp = deps.gimp;
22
24
  this.sessionId = deps.sessionId;
23
25
  this.logger = deps.logger;
24
26
  }
@@ -40,6 +42,7 @@ export class Kernel {
40
42
  executeScript: (innerBody, timeoutMs) => runScript(this.connection, innerBody, timeoutMs),
41
43
  snippet: this.snippetFor(manifest),
42
44
  detection: this.detection,
45
+ gimp: this.gimp,
43
46
  session: { id: this.sessionId },
44
47
  logger: new Logger(`module:${manifest.id}`),
45
48
  };