editmamei 1.0.3 → 1.2.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 (44) hide show
  1. package/README.md +9 -9
  2. package/dist/api/extendscript/_helpers.js +70 -2
  3. package/dist/api/photoshop-api.js +17 -2
  4. package/dist/bin/editmamei-core-darwin-arm64 +0 -0
  5. package/dist/bin/editmamei-core-darwin-x64 +0 -0
  6. package/dist/bin/editmamei-core-win-x64.exe +0 -0
  7. package/dist/cli/activate.js +1 -1
  8. package/dist/cli/config.js +1 -1
  9. package/dist/cli/deactivate.js +1 -1
  10. package/dist/core/raw-develop-state.js +13 -0
  11. package/dist/core/server.js +147 -18
  12. package/dist/core/tool-groups.js +18 -21
  13. package/dist/core/tool-tiers.js +7 -10
  14. package/dist/detection/runtime.js +2 -2
  15. package/dist/modules/ce/index.js +2 -0
  16. package/dist/perception/region-scorer.js +1 -1
  17. package/dist/platform/connection.js +15 -0
  18. package/dist/skills/editmamei-skill.zip +0 -0
  19. package/dist/telemetry/client.js +15 -2
  20. package/dist/telemetry/sanitize.js +1 -0
  21. package/dist/tools/adjustment-tools.js +21 -3
  22. package/dist/tools/detection-tools.js +1 -1
  23. package/dist/tools/document-tools.js +5 -2
  24. package/dist/tools/filter-tools.js +118 -15
  25. package/dist/tools/group-tools.js +146 -146
  26. package/dist/tools/history-tools.js +1 -1
  27. package/dist/tools/inspect-tools.js +22 -3
  28. package/dist/tools/layer-tools.js +4 -27
  29. package/dist/tools/overview-tools.js +37 -8
  30. package/dist/tools/preview-tools.js +3 -2
  31. package/dist/tools/scene-tools.js +1 -1
  32. package/dist/tools/selection-tools.js +91 -18
  33. package/dist/tools/sky-tools.js +126 -0
  34. package/dist/tools/smart-object-tools.js +163 -0
  35. package/dist/tools/text-tools.js +62 -41
  36. package/dist/update/check.js +65 -6
  37. package/dist/update/session-fixes.js +33 -0
  38. package/dist/utils/jsx.js +2 -1
  39. package/dist/utils/operation-timeouts.js +2 -0
  40. package/dist/utils/session-log.js +46 -6
  41. package/dist/utils/temp.js +1 -1
  42. package/dist/utils/tool-helpers.js +23 -0
  43. package/dist/version.js +1 -1
  44. package/package.json +13 -2
@@ -1,10 +1,33 @@
1
1
  import { VERSION } from '../version.js';
2
2
  import { resolveInstallChannel } from '../install-channel.js';
3
- const DEFAULT_DIST_TAGS_URL = 'https://registry.npmjs.org/-/package/editmamei/dist-tags';
3
+ const DEFAULT_LATEST_MANIFEST_URL = 'https://registry.npmjs.org/editmamei/latest';
4
4
  const RELEASES_URL = 'https://editmamei.com/download';
5
5
  export function resolveUpdateCheckUrl(env = process.env) {
6
6
  const override = env.EDITMAMEI_UPDATE_CHECK_URL;
7
- return override && override.length > 0 ? override : DEFAULT_DIST_TAGS_URL;
7
+ return override && override.length > 0 ? override : DEFAULT_LATEST_MANIFEST_URL;
8
+ }
9
+ const MAX_FIX_VERSIONS = 16;
10
+ const MAX_TOOLS_PER_VERSION = 16;
11
+ const MAX_TOOL_NAME_LENGTH = 64;
12
+ export function parseFixesByVersion(raw) {
13
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
14
+ return {};
15
+ const out = {};
16
+ let versions = 0;
17
+ for (const [version, tools] of Object.entries(raw)) {
18
+ if (versions >= MAX_FIX_VERSIONS)
19
+ break;
20
+ if (!parseSemver(version) || !Array.isArray(tools))
21
+ continue;
22
+ const names = tools
23
+ .filter((t) => typeof t === 'string' && t.length > 0 && t.length <= MAX_TOOL_NAME_LENGTH)
24
+ .slice(0, MAX_TOOLS_PER_VERSION);
25
+ if (names.length > 0) {
26
+ out[version] = names;
27
+ versions++;
28
+ }
29
+ }
30
+ return out;
8
31
  }
9
32
  export function httpFetchLatest() {
10
33
  return async (url, timeoutMs) => {
@@ -18,7 +41,12 @@ export function httpFetchLatest() {
18
41
  if (!res.ok)
19
42
  return null;
20
43
  const data = (await res.json());
21
- return typeof data.latest === 'string' ? data.latest : null;
44
+ if (typeof data.version !== 'string')
45
+ return null;
46
+ return {
47
+ version: data.version,
48
+ fixesByVersion: parseFixesByVersion(data.editmamei?.fixesByVersion),
49
+ };
22
50
  }
23
51
  catch {
24
52
  return null;
@@ -43,6 +71,30 @@ export function isNewer(latest, current) {
43
71
  }
44
72
  return false;
45
73
  }
74
+ export function fixedToolsSince(fixesByVersion, current, latest) {
75
+ const versions = Object.keys(fixesByVersion)
76
+ .filter((v) => isNewer(v, current) && !isNewer(v, latest))
77
+ .sort((a, b) => {
78
+ const pa = parseSemver(a);
79
+ const pb = parseSemver(b);
80
+ for (let i = 0; i < 3; i++) {
81
+ if (pa[i] !== pb[i])
82
+ return pa[i] - pb[i];
83
+ }
84
+ return 0;
85
+ });
86
+ const seen = new Set();
87
+ const out = [];
88
+ for (const v of versions) {
89
+ for (const tool of fixesByVersion[v]) {
90
+ if (!seen.has(tool)) {
91
+ seen.add(tool);
92
+ out.push(tool);
93
+ }
94
+ }
95
+ }
96
+ return out;
97
+ }
46
98
  export function updateMessage(channel, latest) {
47
99
  switch (channel) {
48
100
  case 'mcpb':
@@ -64,12 +116,19 @@ export async function checkForUpdate(opts = {}) {
64
116
  const current = opts.current ?? VERSION;
65
117
  const channel = resolveInstallChannel(env);
66
118
  const fetchLatest = opts.fetchLatest ?? httpFetchLatest();
67
- const latest = await fetchLatest(resolveUpdateCheckUrl(env), opts.timeoutMs ?? 4000);
68
- if (!latest || !parseSemver(latest))
119
+ const manifest = await fetchLatest(resolveUpdateCheckUrl(env), opts.timeoutMs ?? 4000);
120
+ if (!manifest || !parseSemver(manifest.version))
69
121
  return null;
122
+ const latest = manifest.version;
70
123
  if (!isNewer(latest, current))
71
124
  return null;
72
- return { current, latest, channel, how_to_update: updateMessage(channel, latest) };
125
+ return {
126
+ current,
127
+ latest,
128
+ channel,
129
+ how_to_update: updateMessage(channel, latest),
130
+ fixed_tools: fixedToolsSince(manifest.fixesByVersion, current, latest),
131
+ };
73
132
  }
74
133
  catch {
75
134
  return null;
@@ -0,0 +1,33 @@
1
+ import { listRecentSessionIds, readSessionLog, } from '../utils/session-log-reader.js';
2
+ const RECENT_SESSION_SCAN_LIMIT = 5;
3
+ export async function previousSessionFailureCounts(currentSessionId, opts = {}) {
4
+ const ids = await listRecentSessionIds(RECENT_SESSION_SCAN_LIMIT, opts);
5
+ for (const id of ids) {
6
+ if (id === currentSessionId)
7
+ continue;
8
+ const entries = await readSessionLog(id, opts);
9
+ const counts = new Map();
10
+ let sawCall = false;
11
+ for (const entry of entries) {
12
+ if ('type' in entry && entry.type === 'meta')
13
+ continue;
14
+ const call = entry;
15
+ if (typeof call.tool !== 'string')
16
+ continue;
17
+ sawCall = true;
18
+ if (call.success === false) {
19
+ counts.set(call.tool, (counts.get(call.tool) ?? 0) + 1);
20
+ }
21
+ }
22
+ if (sawCall)
23
+ return counts;
24
+ }
25
+ return new Map();
26
+ }
27
+ export function relevantFixes(failureCounts, fixedTools, cap = 3) {
28
+ return fixedTools
29
+ .filter((tool) => (failureCounts.get(tool) ?? 0) > 0)
30
+ .map((tool) => ({ tool, failures: failureCounts.get(tool) }))
31
+ .sort((a, b) => b.failures - a.failures)
32
+ .slice(0, cap);
33
+ }
package/dist/utils/jsx.js CHANGED
@@ -1,5 +1,6 @@
1
+ const NON_ASCII = /[^ -~]/g;
1
2
  export function jsLit(value) {
2
- return JSON.stringify(String(value));
3
+ return JSON.stringify(String(value)).replace(NON_ASCII, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, '0')}`);
3
4
  }
4
5
  export function jsNum(value, fallback) {
5
6
  const n = typeof value === 'number' ? value : Number(value);
@@ -3,5 +3,7 @@ export const OPEN_DOCUMENT_REPROBE_TIMEOUT_MS = 10_000;
3
3
  export const CAMERA_RAW_FILTER_TIMEOUT_MS = 120_000;
4
4
  export const SELECT_SUBJECT_TIMEOUT_MS = 120_000;
5
5
  export const SELECT_SKY_TIMEOUT_MS = 120_000;
6
+ export const SELECT_FOCUS_AREA_TIMEOUT_MS = 120_000;
7
+ export const SKY_REPLACEMENT_TIMEOUT_MS = 120_000;
6
8
  export const ANNOTATED_PREVIEW_TIMEOUT_MS = 90_000;
7
9
  export const SCENE_CHANNEL_TIMEOUT_MS = 120_000;
@@ -23,21 +23,58 @@ export function generateSessionId(now = new Date()) {
23
23
  return `${iso}-${suffix}`;
24
24
  }
25
25
  export const ERROR_CLASS_TABLE = [
26
+ { errorClass: 'ps_empty_error', pattern: /returned an empty error|failed with no message/i },
27
+ { errorClass: 'wrong_layer_kind', pattern: /is a group, not an art layer/i },
28
+ {
29
+ errorClass: 'layer_not_found',
30
+ pattern: /\blayer not found|no layer named|(?:layer_to_move|target_layer_name) not found|layer "[^"]*" not found/i,
31
+ },
32
+ { errorClass: 'group_not_found', pattern: /group not found/i },
33
+ { errorClass: 'channel_not_found', pattern: /channel not found|channel named/i },
34
+ { errorClass: 'path_not_found', pattern: /no path named|no paths to|no work path/i },
35
+ { errorClass: 'font_not_found', pattern: /font not found/i },
36
+ {
37
+ errorClass: 'file_not_found',
38
+ pattern: /file not found|map not found|lut not found|could not open lut/i,
39
+ },
40
+ { errorClass: 'face_not_found', pattern: /no face mesh|no face detected/i },
26
41
  {
27
42
  errorClass: 'schema_validation',
28
- pattern: /\bvalidat|required.*field|must be.*type|invalid (input|argument)/i,
43
+ pattern: /\bvalidat|required.*field|missing required argument|must be.*type|invalid (input|argument)/i,
29
44
  },
30
- { errorClass: 'layer_not_found', pattern: /layer .* not found|no layer named/i },
31
- { errorClass: 'ps_command_unavailable', pattern: /not currently available/i },
32
- { errorClass: 'timeout', pattern: /timed? ?out|Script execution timeout|exceeded.*bytes/i },
33
45
  {
34
- errorClass: 'ps_modal_blocking',
35
- pattern: /modal.*dialog|dialog.*blocking|blocked.*modal|photoshop.*modal/i,
46
+ errorClass: 'invalid_argument',
47
+ pattern: /unknown [^:]{1,30}:|invalid |illegal argument|must be |out of bounds|unsupported/i,
36
48
  },
49
+ { errorClass: 'ps_not_detected', pattern: /photoshop info not available/i },
37
50
  {
38
51
  errorClass: 'ps_not_running',
39
52
  pattern: /CreateObject|photoshop.*not.*running|cannot connect.*photoshop|connection.*failed/i,
40
53
  },
54
+ { errorClass: 'no_document', pattern: /no active document|no document is open/i },
55
+ { errorClass: 'no_active_layer', pattern: /no active layer|document has no layers/i },
56
+ {
57
+ errorClass: 'no_selection',
58
+ pattern: /no active selection|requires an active selection|make a selection/i,
59
+ },
60
+ { errorClass: 'background_layer', pattern: /background layer/i },
61
+ { errorClass: 'layer_locked', pattern: /is locked|fully locked|locked layer/i },
62
+ {
63
+ errorClass: 'wrong_layer_kind',
64
+ pattern: /pixel layer|text layer|smart object layer|layer kind|rasterize it first/i,
65
+ },
66
+ { errorClass: 'ps_command_unavailable', pattern: /not currently available/i },
67
+ { errorClass: 'timeout', pattern: /timed? ?out|Script execution timeout|exceeded.*bytes/i },
68
+ {
69
+ errorClass: 'ps_modal_blocking',
70
+ pattern: /modal.*dialog|dialog.*blocking|blocked.*modal|photoshop.*modal/i,
71
+ },
72
+ { errorClass: 'ai_selection_no_result', pattern: /returned no result/i },
73
+ { errorClass: 'ps_general_error', pattern: /general photoshop error/i },
74
+ { errorClass: 'ps_no_such_element', pattern: /no such element/i },
75
+ { errorClass: 'write_not_verified', pattern: /did not verify/i },
76
+ { errorClass: 'ps_empty_error', pattern: /:\s*$/ },
77
+ { errorClass: 'ps_op_failed', pattern: /failed[: (]/i },
41
78
  ];
42
79
  export function classifyError(error) {
43
80
  if (error === undefined)
@@ -174,6 +211,9 @@ export class SessionLog {
174
211
  get path() {
175
212
  return join(this.dir, `${this.sessionId}.ndjson`);
176
213
  }
214
+ get directory() {
215
+ return this.dir;
216
+ }
177
217
  setMcpClientGetter(fn) {
178
218
  this.getMcpClientFn = fn;
179
219
  }
@@ -53,7 +53,7 @@ export class TempDir {
53
53
  const fbMsg = errorString(fbErr);
54
54
  throw new Error(`TempDir.create: tmpdir() at ${tmpdir()} was not writable (${primaryMsg}); ` +
55
55
  `user-owned fallback ${fbRoot} also failed (${fbMsg}). ` +
56
- `Most common cause: TMPDIR inherited from a sudo session — start a fresh shell.`);
56
+ `Most common cause: TMPDIR inherited from a sudo session — start a fresh shell.`, { cause: fbErr });
57
57
  }
58
58
  }
59
59
  }
@@ -17,6 +17,17 @@ export function toolErrorResult(prefix, error) {
17
17
  isError: true,
18
18
  };
19
19
  }
20
+ export function unknownDiscriminator(kind, value, allowed) {
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: `Error: unknown ${kind} "${String(value)}". Allowed: ${allowed.join(', ')}.`,
26
+ },
27
+ ],
28
+ isError: true,
29
+ };
30
+ }
20
31
  export async function runSnippetTool(spec) {
21
32
  try {
22
33
  const args = validateArgs(spec.schema, spec.rawArgs);
@@ -41,3 +52,15 @@ export function applyToActiveLayerProp(op) {
41
52
  default: false,
42
53
  };
43
54
  }
55
+ export function asSmartFilterProp() {
56
+ return {
57
+ type: 'boolean',
58
+ description: 'If true, apply the filter as a re-editable SMART FILTER riding the Smart Object ' +
59
+ 'instead of baking it into pixels — nothing is rasterized, and the filter can later be ' +
60
+ 'toggled, re-blended or removed instead of being permanent. Requires the target to be a ' +
61
+ 'Smart Object (convert first with ps_convert_to_smart_object); errors rather than ' +
62
+ 'converting silently. If false (default), the filter is baked and a smart-object layer ' +
63
+ 'is rasterized first.',
64
+ default: false,
65
+ };
66
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '1.0.3';
1
+ export const VERSION = '1.2.0';
package/package.json CHANGED
@@ -1,8 +1,19 @@
1
1
  {
2
2
  "name": "editmamei",
3
- "version": "1.0.3",
3
+ "version": "1.2.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
+ "editmamei": {
7
+ "fixesByVersion": {
8
+ "1.1.0": [
9
+ "ps_create_clipping_mask"
10
+ ],
11
+ "1.2.0": [
12
+ "ps_delete_layer",
13
+ "ps_select_layer"
14
+ ]
15
+ }
16
+ },
6
17
  "main": "dist/index.js",
7
18
  "type": "module",
8
19
  "bin": {
@@ -45,7 +56,7 @@
45
56
  "url": "https://github.com/editmamei/editmamei/issues"
46
57
  },
47
58
  "engines": {
48
- "node": ">=20.0.0"
59
+ "node": ">=22.0.0"
49
60
  },
50
61
  "dependencies": {
51
62
  "@modelcontextprotocol/sdk": "^1.0.4",