editmamei 1.5.1 → 1.7.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 (46) hide show
  1. package/README.md +22 -1
  2. package/dist/backends/detect-editors.js +79 -0
  3. package/dist/backends/gimp/backend.js +99 -0
  4. package/dist/backends/gimp/bridge/lib.py +1988 -0
  5. package/dist/backends/gimp/bridge/ops.py +3120 -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 +632 -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/preview-privacy.js +54 -0
  15. package/dist/core/server.js +98 -49
  16. package/dist/core/settings.js +25 -0
  17. package/dist/core/tool-activity.js +122 -0
  18. package/dist/core/tool-groups.js +24 -0
  19. package/dist/core/tool-tiers.js +24 -0
  20. package/dist/diagnostics/collect.js +4 -0
  21. package/dist/index.js +5 -1
  22. package/dist/kernel/kernel.js +3 -0
  23. package/dist/modules/gimp/index.js +43 -0
  24. package/dist/skills/editmamei-skill.zip +0 -0
  25. package/dist/telemetry/activity.js +1 -93
  26. package/dist/tools/adjustment-tools.js +2 -1
  27. package/dist/tools/gimp-adjustment-tools.js +385 -0
  28. package/dist/tools/gimp-checkpoint-tools.js +541 -0
  29. package/dist/tools/gimp-compose-tools.js +399 -0
  30. package/dist/tools/gimp-core-tools.js +302 -0
  31. package/dist/tools/gimp-document-tools.js +321 -0
  32. package/dist/tools/gimp-effect-tools.js +267 -0
  33. package/dist/tools/gimp-filter-tools.js +94 -0
  34. package/dist/tools/gimp-geometry-tools.js +223 -0
  35. package/dist/tools/gimp-inspect-tools.js +157 -0
  36. package/dist/tools/gimp-layer-tools.js +385 -0
  37. package/dist/tools/gimp-mask-tools.js +102 -0
  38. package/dist/tools/gimp-shared.js +58 -0
  39. package/dist/tools/gimp-verify-tools.js +409 -0
  40. package/dist/utils/gimp-path.js +27 -0
  41. package/dist/utils/operation-timeouts.js +24 -0
  42. package/dist/utils/session-log.js +19 -1
  43. package/dist/utils/tool-helpers.js +14 -0
  44. package/dist/utils/validate.js +11 -0
  45. package/dist/version.js +1 -1
  46. package/package.json +3 -3
@@ -0,0 +1,54 @@
1
+ function isImageBlock(value) {
2
+ if (typeof value !== 'object' || value === null)
3
+ return false;
4
+ const block = value;
5
+ if (block.type === 'image') {
6
+ return typeof block.data === 'string' && typeof block.mimeType === 'string';
7
+ }
8
+ return (block.type === 'resource' &&
9
+ typeof block.resource?.blob === 'string' &&
10
+ typeof block.resource.mimeType === 'string' &&
11
+ block.resource.mimeType.startsWith('image/'));
12
+ }
13
+ function strip(value, counter) {
14
+ if (Array.isArray(value)) {
15
+ const out = [];
16
+ for (const item of value) {
17
+ if (isImageBlock(item))
18
+ counter.n++;
19
+ else
20
+ out.push(strip(item, counter));
21
+ }
22
+ return out;
23
+ }
24
+ if (typeof value === 'object' && value !== null) {
25
+ const out = {};
26
+ for (const [k, v] of Object.entries(value))
27
+ out[k] = strip(v, counter);
28
+ return out;
29
+ }
30
+ return value;
31
+ }
32
+ export function hasImageContent(result) {
33
+ const counter = { n: 0 };
34
+ strip(result, counter);
35
+ return counter.n > 0;
36
+ }
37
+ export function withholdImages(result) {
38
+ const counter = { n: 0 };
39
+ const stripped = strip(result, counter);
40
+ if (counter.n === 0)
41
+ return result;
42
+ const what = counter.n === 1 ? 'An image was' : `${counter.n} images were`;
43
+ return {
44
+ ...stripped,
45
+ content: [
46
+ ...(stripped.content ?? []),
47
+ {
48
+ type: 'text',
49
+ text: `${what} withheld because the user's privacy.send_previews_to_llm setting is off. ` +
50
+ 'Work from the text and numbers; ask the user if you need to see the image.',
51
+ },
52
+ ],
53
+ };
54
+ }
@@ -5,6 +5,8 @@ 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';
9
+ import { hasImageContent, withholdImages } from './preview-privacy.js';
8
10
  import { EDITION } from '../edition.js';
9
11
  import { VERSION } from '../version.js';
10
12
  import { Session } from './session.js';
@@ -29,6 +31,9 @@ import { ceModule } from '../modules/ce/index.js';
29
31
  import { hostDetectionRuntime } from '../detection/runtime.js';
30
32
  import { ModuleLifecycle, classifyModuleOutcome } from '../kernel/module-lifecycle.js';
31
33
  import { unsupportedHostReason } from '../platform/host-platform.js';
34
+ import { gimpModule } from '../modules/gimp/index.js';
35
+ import { GimpBackend } from '../backends/gimp/backend.js';
36
+ import { createDiagnosticsTools } from '../tools/diagnostics-tools.js';
32
37
  export { classifyModuleOutcome };
33
38
  let logScriptOnErrorWarned = false;
34
39
  function warnLogScriptOnErrorOnce(logger) {
@@ -68,6 +73,9 @@ export class EditmameiServer {
68
73
  session;
69
74
  sessionLog;
70
75
  telemetry;
76
+ editorOpts;
77
+ gimpBackend;
78
+ lastLoggedGimpVersion;
71
79
  kernel;
72
80
  moduleLifecycle;
73
81
  get moduleSkipReason() {
@@ -83,6 +91,7 @@ export class EditmameiServer {
83
91
  updateInfo = null;
84
92
  updateCheck = null;
85
93
  updateNoticeShown = false;
94
+ previewsAllowed = () => loadSettings().settings.privacy.send_previews_to_llm;
86
95
  snippetClient = new GoSnippetClient();
87
96
  refreshLicenseOnPing = createPingLicenseRefresher();
88
97
  moduleUpdateFailed = false;
@@ -91,6 +100,18 @@ export class EditmameiServer {
91
100
  licenseAdvisoryShown = false;
92
101
  constructor(opts = {}) {
93
102
  this.licenseStoreOptions = opts.licenseStore ?? {};
103
+ this.editorOpts = opts.editors ?? {
104
+ registerPhotoshop: true,
105
+ registerGimp: false,
106
+ gimpInstall: null,
107
+ gimpDetectionTimedOut: false,
108
+ };
109
+ if (this.editorOpts.registerGimp) {
110
+ this.gimpBackend = new GimpBackend(this.editorOpts.gimpInstall, {
111
+ gimpDetectionTimedOut: this.editorOpts.gimpDetectionTimedOut,
112
+ gimpPath: this.editorOpts.gimpPathOverride,
113
+ });
114
+ }
94
115
  this.logger = new Logger('EditmameiServer');
95
116
  this.session = new Session();
96
117
  this.sessionLog = new SessionLog(this.session.getSessionId());
@@ -127,6 +148,13 @@ export class EditmameiServer {
127
148
  this.resolveLiveVersionInBackground();
128
149
  }
129
150
  }
151
+ if (entry.success && entry.tool.startsWith('gimp_') && this.gimpBackend?.gimpVersion) {
152
+ const version = this.gimpBackend.gimpVersion;
153
+ if (version !== this.lastLoggedGimpVersion) {
154
+ this.lastLoggedGimpVersion = version;
155
+ void this.sessionLog.setGimpVersion(version);
156
+ }
157
+ }
130
158
  const resultBytes = computeResultBytes(entry.result);
131
159
  void this.sessionLog
132
160
  .append({
@@ -219,52 +247,53 @@ export class EditmameiServer {
219
247
  };
220
248
  }
221
249
  registerTools() {
222
- this.toolRegistry.register('ps_ping', {
223
- tool: {
224
- name: 'ps_ping',
225
- 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).',
226
- inputSchema: {
227
- type: 'object',
228
- properties: {},
229
- },
230
- outputSchema: {
231
- type: 'object',
232
- properties: {
233
- connected: { type: 'boolean' },
234
- version: { type: 'string' },
235
- custom_action_sets: { type: 'number' },
236
- user_templates: { type: 'number' },
237
- open_documents: { type: 'array', items: { type: 'string' } },
238
- degraded: {
239
- type: 'array',
240
- items: { type: 'string' },
241
- 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.',
242
- },
243
- update_available: {
244
- type: ['object', 'null'],
245
- 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`.',
246
- properties: {
247
- current: { type: 'string' },
248
- latest: { type: 'string' },
249
- channel: { type: 'string' },
250
- how_to_update: { type: 'string' },
251
- fixed_tools: { type: 'array', items: { type: 'string' } },
250
+ if (this.editorOpts.registerPhotoshop)
251
+ this.toolRegistry.register('ps_ping', {
252
+ tool: {
253
+ name: 'ps_ping',
254
+ 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).',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {},
258
+ },
259
+ outputSchema: {
260
+ type: 'object',
261
+ properties: {
262
+ connected: { type: 'boolean' },
263
+ version: { type: 'string' },
264
+ custom_action_sets: { type: 'number' },
265
+ user_templates: { type: 'number' },
266
+ open_documents: { type: 'array', items: { type: 'string' } },
267
+ degraded: {
268
+ type: 'array',
269
+ items: { type: 'string' },
270
+ 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.',
271
+ },
272
+ update_available: {
273
+ type: ['object', 'null'],
274
+ 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`.',
275
+ properties: {
276
+ current: { type: 'string' },
277
+ latest: { type: 'string' },
278
+ channel: { type: 'string' },
279
+ how_to_update: { type: 'string' },
280
+ fixed_tools: { type: 'array', items: { type: 'string' } },
281
+ },
282
+ },
283
+ notify_user: {
284
+ type: 'boolean',
285
+ 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.',
252
286
  },
253
- },
254
- notify_user: {
255
- type: 'boolean',
256
- 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.',
257
287
  },
258
288
  },
289
+ annotations: {
290
+ title: 'Ping Photoshop',
291
+ readOnlyHint: true,
292
+ idempotentHint: true,
293
+ },
259
294
  },
260
- annotations: {
261
- title: 'Ping Photoshop',
262
- readOnlyHint: true,
263
- idempotentHint: true,
264
- },
265
- },
266
- handler: async () => await this.pingPhotoshop(),
267
- });
295
+ handler: async () => await this.pingPhotoshop(),
296
+ });
268
297
  this.toolRegistry.register('ps_list_capabilities', {
269
298
  tool: {
270
299
  name: 'ps_list_capabilities',
@@ -315,6 +344,7 @@ export class EditmameiServer {
315
344
  connection: this.session.getConnection(),
316
345
  snippet: this.snippetClient,
317
346
  detection: hostDetectionRuntime(),
347
+ gimp: this.gimpBackend,
318
348
  resolveModuleSnippet: (manifest) => manifest.goCoreSnippets && manifest.goCoreSnippets.length > 0 && proModule
319
349
  ? new GoSnippetClient({ binaryPath: join(proModule.binDir, coreBinaryName()) })
320
350
  : null,
@@ -322,11 +352,22 @@ export class EditmameiServer {
322
352
  logger: this.logger,
323
353
  });
324
354
  this.moduleLifecycle.setKernel(this.kernel);
325
- this.kernel.loadBuiltins([ceModule]);
355
+ const builtins = [];
356
+ if (this.editorOpts.registerPhotoshop)
357
+ builtins.push(ceModule);
358
+ if (this.editorOpts.registerGimp)
359
+ builtins.push(gimpModule);
360
+ this.kernel.loadBuiltins(builtins);
326
361
  this.assertToolsClassified();
362
+ if (!this.editorOpts.registerPhotoshop) {
363
+ const diagnosticsDefs = createDiagnosticsTools(this.session.getConnection(), this.snippetClient).filter((def) => def.tool.name === 'ps_report_problem');
364
+ this.toolRegistry.registerAll(diagnosticsDefs);
365
+ }
327
366
  this.logger.info(`Registered ${this.toolRegistry.count()} tools (edition: ${EDITION})`);
328
367
  }
329
368
  async loadModules() {
369
+ if (!this.editorOpts.registerPhotoshop)
370
+ return;
330
371
  return this.moduleLifecycle.loadModules();
331
372
  }
332
373
  computeModuleStatus() {
@@ -397,6 +438,8 @@ export class EditmameiServer {
397
438
  const result = await this.toolRegistry.execute(name, args);
398
439
  this.session.updateActivity();
399
440
  this.trackRawDevelopState(name, result);
441
+ if (hasImageContent(result) && !this.previewsAllowed())
442
+ return withholdImages(result);
400
443
  return result;
401
444
  }
402
445
  catch (error) {
@@ -409,7 +452,7 @@ export class EditmameiServer {
409
452
  return;
410
453
  if (name === 'ps_open_document') {
411
454
  const sc = result.structuredContent;
412
- if (sc?.is_raw_source === true && this.toolRegistry.get('ps_apply_camera_raw')) {
455
+ if (sc?.is_raw_source === true && this.toolRegistry.get(CAMERA_RAW_TOOL)) {
413
456
  const opened = sc;
414
457
  markRawOpened(String(opened.document_name ?? ''), String(opened.file_path ?? ''));
415
458
  }
@@ -417,12 +460,12 @@ export class EditmameiServer {
417
460
  clearPendingRawDevelop();
418
461
  }
419
462
  }
420
- else if (name === 'ps_develop_raw') {
463
+ else if (name === RAW_DEVELOP_TOOL) {
421
464
  const sc = result.structuredContent;
422
465
  if (sc?.opened === true)
423
466
  clearPendingRawDevelop();
424
467
  }
425
- else if (name === 'ps_apply_camera_raw' || name === 'ps_close_document') {
468
+ else if (name === CAMERA_RAW_TOOL || name === 'ps_close_document') {
426
469
  clearPendingRawDevelop();
427
470
  }
428
471
  }
@@ -667,18 +710,24 @@ export class EditmameiServer {
667
710
  await this.loadModules();
668
711
  const transport = new StdioServerTransport();
669
712
  await this.server.connect(transport);
670
- void this.session.initialize().catch(() => undefined);
671
- void this.reprovisionIfModuleSkipped();
672
- void this.ensureEntitledModuleFresh();
713
+ if (this.editorOpts.registerPhotoshop) {
714
+ void this.session.initialize().catch(() => undefined);
715
+ void this.reprovisionIfModuleSkipped();
716
+ void this.ensureEntitledModuleFresh();
717
+ }
673
718
  this.telemetry.start();
674
719
  void this.telemetry.flushOutboxOnStartup();
675
720
  this.server.onclose = () => {
676
721
  void this.telemetry.shutdown();
722
+ if (this.gimpBackend)
723
+ void this.gimpBackend.shutdown();
677
724
  };
678
725
  this.logger.info('Editmamei is listening on stdio');
679
726
  }
680
727
  async stop() {
681
728
  await this.telemetry.shutdown();
729
+ if (this.gimpBackend)
730
+ await this.gimpBackend.shutdown();
682
731
  await this.session.disconnect();
683
732
  await this.sessionLog.close();
684
733
  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,122 @@
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_add_effect',
108
+ 'gimp_filter',
109
+ 'gimp_crop_document',
110
+ 'gimp_resize_image',
111
+ 'gimp_transform_canvas',
112
+ 'gimp_create_mask',
113
+ 'gimp_checkpoint',
114
+ 'gimp_layer',
115
+ 'gimp_bake',
116
+ 'gimp_create_document',
117
+ 'gimp_place_image',
118
+ 'gimp_canvas',
119
+ 'gimp_convert_image_mode',
120
+ ]);
121
+ export const RAW_DEVELOP_TOOL = 'ps_develop_raw';
122
+ export const CAMERA_RAW_TOOL = 'ps_apply_camera_raw';
@@ -171,6 +171,30 @@ 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_add_effect: 'filter',
190
+ gimp_create_mask: 'masks',
191
+ gimp_checkpoint: 'document',
192
+ gimp_layer: 'layers',
193
+ gimp_bake: 'layers',
194
+ gimp_create_document: 'document',
195
+ gimp_place_image: 'layers',
196
+ gimp_canvas: 'document',
197
+ gimp_convert_image_mode: 'document',
174
198
  };
175
199
  export function groupOf(toolName) {
176
200
  const group = TOOL_GROUPS[toolName];
@@ -89,6 +89,30 @@ 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_add_effect: 'community',
102
+ gimp_crop_document: 'community',
103
+ gimp_resize_image: 'community',
104
+ gimp_transform_canvas: 'community',
105
+ gimp_create_mask: 'community',
106
+ gimp_get_preview: 'community',
107
+ gimp_get_histogram: 'community',
108
+ gimp_compare: 'community',
109
+ gimp_checkpoint: 'community',
110
+ gimp_layer: 'community',
111
+ gimp_bake: 'community',
112
+ gimp_create_document: 'community',
113
+ gimp_place_image: 'community',
114
+ gimp_canvas: 'community',
115
+ gimp_convert_image_mode: 'community',
92
116
  };
93
117
  export function tierOf(toolName) {
94
118
  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
  };
@@ -0,0 +1,43 @@
1
+ import { KERNEL_ABI } from '../../kernel/host-api.js';
2
+ import { EDITION } from '../../edition.js';
3
+ import { isToolAllowedInEdition } from '../../core/tool-tiers.js';
4
+ import { createGimpCoreTools } from '../../tools/gimp-core-tools.js';
5
+ import { createGimpDocumentTools } from '../../tools/gimp-document-tools.js';
6
+ import { createGimpInspectTools } from '../../tools/gimp-inspect-tools.js';
7
+ import { createGimpAdjustmentTools } from '../../tools/gimp-adjustment-tools.js';
8
+ import { createGimpEffectTools } from '../../tools/gimp-effect-tools.js';
9
+ import { createGimpFilterTools } from '../../tools/gimp-filter-tools.js';
10
+ import { createGimpGeometryTools } from '../../tools/gimp-geometry-tools.js';
11
+ import { createGimpMaskTools } from '../../tools/gimp-mask-tools.js';
12
+ import { createGimpVerifyTools } from '../../tools/gimp-verify-tools.js';
13
+ import { createGimpCheckpointTools } from '../../tools/gimp-checkpoint-tools.js';
14
+ import { createGimpLayerTools } from '../../tools/gimp-layer-tools.js';
15
+ import { createGimpComposeTools } from '../../tools/gimp-compose-tools.js';
16
+ export const gimpFactories = [
17
+ createGimpCoreTools,
18
+ createGimpDocumentTools,
19
+ createGimpInspectTools,
20
+ createGimpAdjustmentTools,
21
+ createGimpEffectTools,
22
+ createGimpFilterTools,
23
+ createGimpGeometryTools,
24
+ createGimpMaskTools,
25
+ createGimpVerifyTools,
26
+ createGimpCheckpointTools,
27
+ createGimpLayerTools,
28
+ createGimpComposeTools,
29
+ ];
30
+ export const gimpModule = {
31
+ manifest: { id: 'gimp', name: 'Editmamei GIMP Tools', abi: KERNEL_ABI },
32
+ register(host) {
33
+ if (!host.gimp) {
34
+ host.logger.warn('gimpModule.register called with no host.gimp backend — skipping.');
35
+ return;
36
+ }
37
+ const gimp = host.gimp;
38
+ const defs = gimpFactories
39
+ .flatMap((f) => f(gimp))
40
+ .filter((def) => isToolAllowedInEdition(def.tool.name, EDITION));
41
+ host.registerTools(defs);
42
+ },
43
+ };