editmamei 1.2.1 → 1.3.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.
Binary file
Binary file
@@ -26,11 +26,19 @@ export async function runRepair(opts = {}) {
26
26
  out(` Installed ${m.sku} module v${m.version}.\n`);
27
27
  for (const s of prov.skipped)
28
28
  out(` Skipped ${s.sku} v${s.version}: ${s.reason}.\n`);
29
- for (const e of prov.errors) {
29
+ const tooNew = prov.errors.filter((e) => e.code === 'abi_too_new');
30
+ const failures = prov.errors.filter((e) => e.code !== 'abi_too_new');
31
+ for (const e of tooNew) {
32
+ out(` The published ${e.sku} module needs a newer Editmamei than this one — ` +
33
+ `update Editmamei to load it. Your installed module is unchanged.\n`);
34
+ }
35
+ for (const e of failures) {
30
36
  err(` Error: could not provision the ${e.sku} module: ${e.message}\n`);
31
37
  }
32
- if (prov.errors.length > 0) {
38
+ if (failures.length > 0) {
33
39
  throw new Error('module re-provisioning failed');
34
40
  }
35
- out('\nRestart your MCP client (Claude Desktop / Claude Code) to load Pro tools.\n');
41
+ if (prov.installed.length > 0 || tooNew.length === 0) {
42
+ out('\nRestart your MCP client (Claude Desktop / Claude Code) to load Pro tools.\n');
43
+ }
36
44
  }
@@ -8,7 +8,7 @@ export const TOOL_TIERS = {
8
8
  ps_batch: 'pro',
9
9
  ps_add_adjustment_layer: 'community',
10
10
  ps_apply_adjustment: 'community',
11
- ps_document: 'dev',
11
+ ps_document: 'community',
12
12
  ps_create_document: 'community',
13
13
  ps_close_document: 'community',
14
14
  ps_open_document: 'community',
@@ -69,7 +69,7 @@ export const TOOL_TIERS = {
69
69
  ps_select_subject_instance: 'pro',
70
70
  ps_select_object: 'pro',
71
71
  ps_select_focus_area: 'dev',
72
- ps_replace_sky: 'dev',
72
+ ps_replace_sky: 'community',
73
73
  ps_modify_selection: 'community',
74
74
  ps_get_selection_preview: 'community',
75
75
  ps_selection_channel: 'community',
@@ -2,6 +2,7 @@ import { DeliveryClient, DeliveryError } from './client.js';
2
2
  import { sha256Hex } from './crypto.js';
3
3
  import { verifyModuleSignature, verifyModuleSignatureV2, digestsRootSha256Hex, isModuleFileDigestArray, } from './signing.js';
4
4
  import { installModule, readInstalledModule } from './store.js';
5
+ import { KERNEL_ABI } from '../kernel/host-api.js';
5
6
  import { Logger } from '../utils/logger.js';
6
7
  const logger = new Logger('Modules');
7
8
  const SKU_RE = /^[a-z0-9-]{2,32}$/;
@@ -52,6 +53,23 @@ export async function provisionModules(key, opts = {}) {
52
53
  });
53
54
  continue;
54
55
  }
56
+ const abi = entry.abi;
57
+ if (!Number.isInteger(abi) || abi < 1) {
58
+ result.errors.push({
59
+ sku,
60
+ code: 'abi_invalid',
61
+ message: `the manifest entry for ${sku} declares an invalid abi '${String(abi)}' (expected a whole number of at least 1) — refusing to install it`,
62
+ });
63
+ continue;
64
+ }
65
+ if (abi > KERNEL_ABI) {
66
+ result.errors.push({
67
+ sku,
68
+ code: 'abi_too_new',
69
+ message: `the published ${sku} module needs a newer Editmamei than this one (module abi ${abi}, this host runs up to abi ${KERNEL_ABI}) — update Editmamei; the installed module is left as it is`,
70
+ });
71
+ continue;
72
+ }
55
73
  const installed = readInstalledModule(sku, opts);
56
74
  if (installed && installed.version === latest && !opts.force) {
57
75
  result.skipped.push({ sku, version: latest, reason: 'up-to-date' });
@@ -119,7 +137,7 @@ export async function provisionModules(key, opts = {}) {
119
137
  installModule({
120
138
  sku,
121
139
  version: latest,
122
- abi: entry.abi,
140
+ abi,
123
141
  sha256: digest,
124
142
  alg: contentKey.alg,
125
143
  content_key: contentKey.key,
@@ -73,9 +73,10 @@ export class Kernel {
73
73
  throw new Error('downloaded module export is not an EditmameiModule (expected a default export with a manifest + register())');
74
74
  }
75
75
  if (this.abiTooNew(mod.manifest))
76
- return;
76
+ return 'abi-too-new';
77
77
  const before = this.registry.count();
78
78
  await mod.register(this.hostApiFor(mod.manifest));
79
79
  this.logger.info(`Loaded downloaded module '${mod.manifest.id}' (${mod.manifest.name}) — ${this.registry.count() - before} tools.`);
80
+ return 'loaded';
80
81
  }
81
82
  }
@@ -7,7 +7,7 @@ import { isProEntitled } from '../license/entitlement.js';
7
7
  import { loadVerifiedModule, readInstalledModule, installedPath, PRO_SKU, } from '../delivery/store.js';
8
8
  import { provisionModules, compareVersions, VERSION_RE, } from '../delivery/provision.js';
9
9
  import { readLicense } from '../license/store.js';
10
- import { HOST_MIN_ABI } from './host-api.js';
10
+ import { HOST_MIN_ABI, KERNEL_ABI } from './host-api.js';
11
11
  import { VERSION } from '../version.js';
12
12
  export function classifyModuleOutcome(inputs) {
13
13
  if (inputs.proModuleLoaded && inputs.skipReason === null)
@@ -79,9 +79,21 @@ export class ModuleLifecycle {
79
79
  this._moduleSkipReason = 'incompatible';
80
80
  return;
81
81
  }
82
+ if (this._proModule.abi !== null && this._proModule.abi > KERNEL_ABI) {
83
+ this.deps.logger.warn(`Pro module (abi ${this._proModule.abi}) needs a newer host than this build ` +
84
+ `(kernel abi ${KERNEL_ABI}) — booting Community; update Editmamei to load it.`);
85
+ this._moduleSkipReason = 'incompatible';
86
+ return;
87
+ }
82
88
  const snapshot = this.deps.toolRegistry.snapshot();
83
89
  try {
84
- await this.kernel.loadDownloaded(this._proModule.importer);
90
+ const outcome = await this.kernel.loadDownloaded(this._proModule.importer);
91
+ if (outcome === 'abi-too-new') {
92
+ this.deps.logger.warn(`The installed Pro module needs a newer host than this build (kernel abi ` +
93
+ `${KERNEL_ABI}) — booting Community; update Editmamei to load it.`);
94
+ this._moduleSkipReason = 'incompatible';
95
+ return;
96
+ }
85
97
  if (this._proModule.version !== null &&
86
98
  VERSION_RE.test(this._proModule.version) &&
87
99
  compareVersions(this._proModule.version, VERSION) > 0) {
@@ -159,11 +171,14 @@ export class ModuleLifecycle {
159
171
  this.deps.logger.warn('Module delivery is not configured — staying Community.');
160
172
  return;
161
173
  }
174
+ const abiTooNew = prov.errors.some((e) => e.code === 'abi_too_new');
162
175
  for (const e of prov.errors) {
176
+ if (e.code === 'abi_too_new')
177
+ continue;
163
178
  this.deps.logger.warn(`Could not re-provision the ${e.sku} module (staying Community): ${e.message}`);
164
179
  }
165
- if (prov.errors.length === 0) {
166
- if (reason === 'incompatible') {
180
+ if (prov.errors.length === 0 || abiTooNew) {
181
+ if (reason === 'incompatible' || abiTooNew) {
167
182
  this.deps.logger.warn('The published Pro module does not yet support this host version — staying ' +
168
183
  'Community. Update Editmamei when a compatible release ships; ' +
169
184
  '`editmamei report` files a diagnostic.');
@@ -201,6 +216,11 @@ export class ModuleLifecycle {
201
216
  this.deps.logger.info(`Pro module updated to v${m.version} — restart to load.`);
202
217
  }
203
218
  for (const e of prov.errors) {
219
+ if (e.code === 'abi_too_new') {
220
+ this.deps.logger.info(`A newer Pro module is published but needs a newer Editmamei than this ` +
221
+ `one — staying on the installed version, which works. Update Editmamei to get it.`);
222
+ continue;
223
+ }
204
224
  this.deps.logger.warn(`Pro-module freshness check could not provision the ${e.sku} module ` +
205
225
  `(staying on the installed version): ${e.message}`);
206
226
  }
@@ -30,7 +30,7 @@ import { createVectorMaskTools } from '../../tools/vector-mask-tools.js';
30
30
  import { createChannelComposeTools } from '../../tools/channel-compose-tools.js';
31
31
  import { createShapeTools } from '../../tools/shape-tools.js';
32
32
  import { createSkyTools } from '../../tools/sky-tools.js';
33
- const ceFactories = [
33
+ export const ceFactories = [
34
34
  createDocumentTools,
35
35
  createLayerTools,
36
36
  createGroupTools,
@@ -254,17 +254,19 @@ export const PLACEMENT_SCHEMA = {
254
254
  type: 'object',
255
255
  description: 'ANCHOR-RELATIONAL placement (preferred over guessing pixels): name anchors + a relation and the ' +
256
256
  'spatial-grounding resolver computes the geometry, verified by an objective gate; the action runs ONLY if the ' +
257
- 'gate PASSES. For a HARD or ambiguous placement, first concur on ps_resolve_placement (review_crop: true) — a ' +
258
- 'read-only zoomed crop with a marker at the resolved spot — then pass the SAME anchors + relation here. See ' +
259
- 'ps_resolve_placement for the full anchors + relation vocabulary.',
257
+ 'gate PASSES. For a HARD or ambiguous placement, first concur with the placement-resolver tool — when this ' +
258
+ 'build has one, it appears in tools/list — by calling it with review_crop: true for a read-only zoomed crop ' +
259
+ 'with a marker at the resolved spot, then pass the SAME anchors + relation here. The full anchors + relation ' +
260
+ 'vocabulary is documented on that tool.',
260
261
  properties: {
261
262
  anchors: {
262
263
  type: 'array',
263
- description: 'Named anchors, same vocabulary as ps_resolve_placement (face/object/grid/extremum/corner/edge/landmark).',
264
+ description: 'Named anchors, same vocabulary as the placement-resolver tool, when this build has one ' +
265
+ '(face/object/grid/extremum/corner/edge/landmark).',
264
266
  },
265
267
  relation: {
266
268
  type: 'object',
267
- description: 'The relation, same vocabulary as ps_resolve_placement.',
269
+ description: 'The relation, same vocabulary as the placement-resolver tool, when this build has one.',
268
270
  },
269
271
  max_dimension: {
270
272
  type: 'number',
Binary file
@@ -246,7 +246,6 @@ export function createDocumentTools(connection, snippetClient) {
246
246
  },
247
247
  annotations: {
248
248
  title: 'List / Activate Documents',
249
- readOnlyHint: true,
250
249
  idempotentHint: true,
251
250
  },
252
251
  },
@@ -29,7 +29,8 @@ const cropDocumentSchema = {
29
29
  ...PLACEMENT_SCHEMA,
30
30
  description: 'ANCHOR-RELATIONAL crop (preferred over guessing pixels): a REGION relation (inside/gap) → the crop is the ' +
31
31
  'resolved region bounding box, verified by the gate. Crops ONLY if the gate PASSES. When set, ' +
32
- 'left/top/right/bottom are ignored. See ps_resolve_placement for the anchors + relation vocabulary.',
32
+ 'left/top/right/bottom are ignored. See the placement-resolver tool, when this build has one, for the ' +
33
+ 'anchors + relation vocabulary.',
33
34
  },
34
35
  left: {
35
36
  type: 'integer',
@@ -82,7 +82,8 @@ const moveLayerSchema = {
82
82
  description: 'ANCHOR-RELATIONAL move (preferred over guessing a pixel): a POINT relation (centroid/midpoint/offset) → ' +
83
83
  'the layer\'s CENTER is moved to the resolved, gate-verified point (e.g. "center this layer on the detected ' +
84
84
  'subject" / "…in the gap between the two people"). Moves ONLY if the gate PASSES. When set, delta_*/' +
85
- 'absolute_*/center_on_* are ignored. See ps_resolve_placement for the anchors + relation vocabulary.',
85
+ 'absolute_*/center_on_* are ignored. See the placement-resolver tool, when this build has one, for the ' +
86
+ 'anchors + relation vocabulary.',
86
87
  },
87
88
  },
88
89
  };
@@ -73,7 +73,7 @@ const pathInputSchema = {
73
73
  tool: {
74
74
  type: 'string',
75
75
  enum: SUPPORTED_BRUSH_TOOLS,
76
- description: "stroke only: which brush-family tool paints the path. Default 'brush'. Same 16-tool set as ps_apply_brush_stroke.",
76
+ description: "stroke only: which brush-family tool paints the path (see this field's own enum for the full supported set). Default 'brush'.",
77
77
  default: 'brush',
78
78
  },
79
79
  color: {
@@ -466,7 +466,7 @@ const SELECT_INPUT_SCHEMA = {
466
466
  ...selectPolygonSchema.properties,
467
467
  placement: {
468
468
  ...PLACEMENT_SCHEMA,
469
- description: 'Grounded coordinates (rectangle/ellipse/magic_wand): NAME anchors + a relation instead of guessing pixels. rectangle/ellipse ← a REGION relation (inside/gap) → the selection bounding box; magic_wand ← a POINT relation (centroid/extremum/grid) → the click. Verified by the objective gate; wins over the raw edges/x-y. See ps_resolve_placement for the vocabulary.',
469
+ description: 'Grounded coordinates (rectangle/ellipse/magic_wand): NAME anchors + a relation instead of guessing pixels. rectangle/ellipse ← a REGION relation (inside/gap) → the selection bounding box; magic_wand ← a POINT relation (centroid/extremum/grid) → the click. Verified by the objective gate; wins over the raw edges/x-y. See the placement-resolver tool, when this build has one, for the vocabulary.',
470
470
  },
471
471
  },
472
472
  required: ['mode'],
@@ -31,7 +31,7 @@ const shapeInputSchema = {
31
31
  'edge or a Pro face-mesh landmark curve) → a straight line between the resolved curve endpoints. The shape ' +
32
32
  'is created ONLY if the gate PASSES (otherwise an error and no layer). When set, left/top/right/bottom and ' +
33
33
  'start_x/start_y/end_x/end_y are ignored, but styling (fill_color/stroke/weight/corner_radius) still ' +
34
- 'applies. See ps_resolve_placement for the anchors + relation vocabulary.',
34
+ 'applies. See the placement-resolver tool, when this build has one, for the anchors + relation vocabulary.',
35
35
  },
36
36
  left: {
37
37
  type: 'number',
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.2.1';
1
+ export const VERSION = '1.3.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editmamei",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Photoshop MCP server: natural-language AI photo editing with your own Photoshop (Community Edition)",
5
5
  "mcpName": "io.github.editmamei/editmamei",
6
6
  "editmamei": {