editmamei 1.0.2 → 1.1.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 (38) hide show
  1. package/README.md +7 -7
  2. package/dist/api/extendscript/_helpers.js +64 -2
  3. package/dist/api/photoshop-api.js +1 -1
  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 +64 -3
  12. package/dist/core/tool-groups.js +14 -12
  13. package/dist/core/tool-tiers.js +4 -2
  14. package/dist/detection/runtime.js +2 -2
  15. package/dist/perception/region-scorer.js +1 -1
  16. package/dist/platform/connection.js +15 -0
  17. package/dist/skills/editmamei-skill.zip +0 -0
  18. package/dist/telemetry/client.js +15 -2
  19. package/dist/telemetry/sanitize.js +1 -0
  20. package/dist/tools/adjustment-tools.js +21 -3
  21. package/dist/tools/detection-tools.js +1 -1
  22. package/dist/tools/document-tools.js +5 -2
  23. package/dist/tools/filter-tools.js +130 -13
  24. package/dist/tools/group-tools.js +172 -54
  25. package/dist/tools/history-tools.js +1 -1
  26. package/dist/tools/inspect-tools.js +22 -3
  27. package/dist/tools/layer-tools.js +4 -4
  28. package/dist/tools/overview-tools.js +37 -8
  29. package/dist/tools/preview-tools.js +3 -2
  30. package/dist/tools/scene-tools.js +1 -1
  31. package/dist/tools/selection-tools.js +19 -17
  32. package/dist/tools/smart-object-tools.js +163 -0
  33. package/dist/tools/text-tools.js +92 -2
  34. package/dist/utils/session-log.js +42 -6
  35. package/dist/utils/temp.js +1 -1
  36. package/dist/utils/tool-helpers.js +23 -0
  37. package/dist/version.js +1 -1
  38. package/package.json +3 -2
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import { runScript } from '../utils/run-script.js';
3
3
  import { TempDir } from '../utils/temp.js';
4
4
  import { validateArgs } from '../utils/validate.js';
5
- import { toolErrorResult, runSnippetTool } from '../utils/tool-helpers.js';
5
+ import { toolErrorResult, runSnippetTool, unknownDiscriminator } from '../utils/tool-helpers.js';
6
6
  import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
7
7
  import { resolveExpectedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
8
8
  import { SELECT_SUBJECT_TIMEOUT_MS, SELECT_SKY_TIMEOUT_MS } from '../utils/operation-timeouts.js';
@@ -450,8 +450,8 @@ const SELECT_INPUT_SCHEMA = {
450
450
  'color_range: target red/green/blue (+fuzziness) — "select all the red / skin". ' +
451
451
  'luminance_range: luminance highlights|shadows|midtones (+fuzziness, lower_limit, upper_limit) — glow/dodge-burn foundation. ' +
452
452
  'magic_wand: click x/y (+tolerance, contiguous, anti_alias, sample_all_layers). ' +
453
- 'grow: expand the CURRENT selection to adjacent similar-colour pixels (+tolerance, anti_alias). ' +
454
- 'similar: expand the CURRENT selection to ALL similar-colour pixels document-wide (+tolerance, anti_alias). ' +
453
+ 'grow: DEPRECATED here — use ps_modify_selection(op=grow) instead (kept for one release for backward compatibility, identical behaviour). Expands the CURRENT selection to adjacent similar-colour pixels (+tolerance, anti_alias). ' +
454
+ 'similar: DEPRECATED here — use ps_modify_selection(op=similar) instead (kept for one release for backward compatibility, identical behaviour). Expands the CURRENT selection to ALL similar-colour pixels document-wide (+tolerance, anti_alias). ' +
455
455
  'skin_tones: select skin-coloured pixels (+fuzziness; use_faces=true adds face-aware refinement). ' +
456
456
  'out_of_gamut: select colours outside the printable CMYK gamut (no params). ' +
457
457
  'polygon: points [{x,y},...] in ABSOLUTE document pixels (min 3, auto-closes) — covers polygonal/freehand lasso. Coordinate-driven: you must know the pixel positions (use ps_inspect / ps_get_preview to aim, or ps_path create_from_placement → load_as_selection for a grounded outline). ' +
@@ -479,6 +479,8 @@ const MODIFY_SELECTION_OPS = [
479
479
  'border',
480
480
  'smooth',
481
481
  'transform',
482
+ 'grow',
483
+ 'similar',
482
484
  ];
483
485
  const MODIFY_SELECTION_INPUT_SCHEMA = {
484
486
  type: 'object',
@@ -491,13 +493,16 @@ const MODIFY_SELECTION_INPUT_SCHEMA = {
491
493
  'expand: grow the selection outward by `amount` px. contract: shrink it inward by `amount` px. ' +
492
494
  'border: replace the selection with a `amount`-px-wide band straddling its edge. ' +
493
495
  'smooth: round off the selection corners with a `amount`-px radius. ' +
496
+ 'grow: expand the selection to adjacent similar-colour pixels (+tolerance, anti_alias). ' +
497
+ 'similar: expand the selection to ALL similar-colour pixels document-wide (+tolerance, anti_alias). ' +
494
498
  'transform: relatively scale (scale_x_percent / scale_y_percent), rotate (rotate_degrees) and/or translate (offset_x / offset_y) the marching ants — not pixels. ' +
495
- 'expand/contract/border/smooth all require an active selection and take `amount`; expand/contract/smooth also take at_canvas_bounds.',
499
+ 'expand/contract/border/smooth all require an active selection and take `amount`; expand/contract/smooth also take at_canvas_bounds. grow/similar also require an active selection.',
496
500
  },
497
501
  ...featherSelectionSchema.properties,
498
502
  ...refineEdgeSchema.properties,
499
503
  ...modifyEdgeSchema.properties,
500
504
  ...transformSelectionSchema.properties,
505
+ ...growSelectionSchema.properties,
501
506
  },
502
507
  required: ['op'],
503
508
  };
@@ -575,7 +580,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
575
580
  {
576
581
  tool: {
577
582
  name: 'ps_select',
578
- description: 'Create or modify a selection — choose with `mode`. `all` selects the canvas; `none` deselects; `inverse` inverts the current selection (e.g. select the subject, then inverse to act on the background). `rectangle` (left/top/right/bottom, optional feather_px to avoid hard block-edges in smooth sky). `ellipse` (left/top/right/bottom bounding box + anti_alias — circles/ovals). `color_range` (target red/green/blue + fuzziness — "select all the red / skin tones"). `luminance_range` (highlights/shadows/midtones — foundation for glow / dodge-burn). `magic_wand` (click x/y + tolerance, contiguous). `grow` / `similar` expand the CURRENT selection to similar-colour pixels (adjacent vs document-wide; both honour `tolerance`). rectangle/ellipse/magic_wand also accept a grounded `placement` (NAME a region/point instead of guessing pixels — resolved + gate-verified). The geometric/color/wand modes take selection_type (replace|add|subtract|intersect) to combine with an existing selection and return a rich selection_info bundle — verify it (or ps_get_selection_preview) before committing to a mask.',
583
+ description: 'Create a NEW selection — choose with `mode`. (To edit the CURRENT selection instead — including growing it by colour similarity — use ps_modify_selection.) `all` selects the canvas; `none` deselects; `inverse` inverts the current selection (e.g. select the subject, then inverse to act on the background). `rectangle` (left/top/right/bottom, optional feather_px to avoid hard block-edges in smooth sky). `ellipse` (left/top/right/bottom bounding box + anti_alias — circles/ovals). `color_range` (target red/green/blue + fuzziness — "select all the red / skin tones"). `luminance_range` (highlights/shadows/midtones — foundation for glow / dodge-burn). `magic_wand` (click x/y + tolerance, contiguous). `grow` / `similar` are DEPRECATED here (they act on the CURRENT selection, not a new one) — use ps_modify_selection(op=grow|similar) instead; kept for one release for backward compatibility, identical behaviour. rectangle/ellipse/magic_wand also accept a grounded `placement` (NAME a region/point instead of guessing pixels — resolved + gate-verified). The geometric/color/wand modes take selection_type (replace|add|subtract|intersect) to combine with an existing selection and return a rich selection_info bundle — verify it (or ps_get_selection_preview) before committing to a mask.',
579
584
  inputSchema: SELECT_INPUT_SCHEMA,
580
585
  outputSchema: {
581
586
  type: 'object',
@@ -614,7 +619,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
614
619
  {
615
620
  tool: {
616
621
  name: 'ps_modify_selection',
617
- description: "Modify the CURRENT selection — choose with `op`. `feather` softens by radius_px (use when a selection was made hard, e.g. after select all/inverse, before applying an adjustment). `refine_edge` runs Select-and-Mask's global sliders headlessly (smooth, feather, contrast, shift_edge, edge-detection radius, decontaminate) to clean halos and soft/fuzzy edges (hair) after a rough color-range / magic-wand / subject selection. `expand` / `contract` grow / shrink the selection by `amount` px. `border` replaces the selection with an `amount`-px band around its edge. `smooth` rounds the corners with an `amount`-px radius. `transform` relatively scales (scale_x_percent / scale_y_percent), rotates (rotate_degrees) and/or translates (offset_x / offset_y) the marching ants — pixels are untouched. All require an active selection and return selection_info.",
622
+ description: "Modify the CURRENT selection — choose with `op`. (To create a NEW selection instead, use ps_select.) `feather` softens by radius_px (use when a selection was made hard, e.g. after select all/inverse, before applying an adjustment). `refine_edge` runs Select-and-Mask's global sliders headlessly (smooth, feather, contrast, shift_edge, edge-detection radius, decontaminate) to clean halos and soft/fuzzy edges (hair) after a rough color-range / magic-wand / subject selection. `expand` / `contract` grow / shrink the selection by `amount` px. `border` replaces the selection with an `amount`-px band around its edge. `smooth` rounds the corners with an `amount`-px radius. `grow` / `similar` expand the selection to similar-colour pixels by `tolerance` (+anti_alias) — `grow` to adjacent pixels only, `similar` document-wide. `transform` relatively scales (scale_x_percent / scale_y_percent), rotates (rotate_degrees) and/or translates (offset_x / offset_y) the marching ants — pixels are untouched. All require an active selection and return selection_info.",
618
623
  inputSchema: MODIFY_SELECTION_INPUT_SCHEMA,
619
624
  outputSchema: {
620
625
  type: 'object',
@@ -623,6 +628,8 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
623
628
  refined: { type: 'boolean' },
624
629
  modified: { type: 'boolean' },
625
630
  transformed: { type: 'boolean' },
631
+ selected: { type: 'boolean', description: 'op=grow/similar: true on success.' },
632
+ method: { type: 'string', description: 'op=grow/similar: "grow" or "similar".' },
626
633
  mode: { type: 'string' },
627
634
  amount_px: { type: 'number' },
628
635
  scale_x_percent: { type: 'number' },
@@ -637,6 +644,8 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
637
644
  contrast: { type: 'number' },
638
645
  shift_edge: { type: 'number' },
639
646
  decontaminate: { type: 'boolean' },
647
+ tolerance: { type: 'number', description: 'op=grow/similar: tolerance used.' },
648
+ anti_alias: { type: 'boolean', description: 'op=grow/similar: anti_alias used.' },
640
649
  output: { type: 'string' },
641
650
  selection_info: selectionInfoFragment,
642
651
  },
@@ -864,17 +873,6 @@ async function selectSky(connection, snippetClient, rawArgs) {
864
873
  successText: (_result, args) => `Select Sky (${normalizeSelectionType(args.selection_type)}) complete`,
865
874
  });
866
875
  }
867
- function unknownDiscriminator(kind, value, allowed) {
868
- return {
869
- content: [
870
- {
871
- type: 'text',
872
- text: `Error: unknown ${kind} "${String(value)}". Allowed: ${allowed.join(', ')}.`,
873
- },
874
- ],
875
- isError: true,
876
- };
877
- }
878
876
  async function select(connection, snippetClient, detClient, rawArgs) {
879
877
  const mode = rawArgs.mode;
880
878
  const { mode: _omit, ...rest } = rawArgs;
@@ -924,6 +922,10 @@ async function modifySelection(connection, snippetClient, rawArgs) {
924
922
  return modifySelectionEdge(connection, snippetClient, op, rest);
925
923
  case 'transform':
926
924
  return transformSelection(connection, snippetClient, rest);
925
+ case 'grow':
926
+ return growSelection(connection, snippetClient, 'grow', rest);
927
+ case 'similar':
928
+ return growSelection(connection, snippetClient, 'similar', rest);
927
929
  default:
928
930
  return unknownDiscriminator('modify_selection op', op, MODIFY_SELECTION_OPS);
929
931
  }
@@ -0,0 +1,163 @@
1
+ import { validateArgs } from '../utils/validate.js';
2
+ import { runSnippetTool, toolErrorResult } from '../utils/tool-helpers.js';
3
+ import { LAYER_BLEND_MODES } from '../utils/blend-modes.js';
4
+ const SMART_FILTER_OPS = ['list', 'set_visibility', 'set_blend', 'remove'];
5
+ const smartFilterInputSchema = {
6
+ type: 'object',
7
+ properties: {
8
+ op: {
9
+ type: 'string',
10
+ enum: [...SMART_FILTER_OPS],
11
+ description: 'list: read every Smart Filter on the active layer (index, name, type, enabled, opacity, blend mode). Read-only — call it first to get the indices the other ops take. ' +
12
+ 'set_visibility: turn one filter on or off without removing it (needs `index` + `enabled`). ' +
13
+ "set_blend: change one filter's `opacity` and/or `blend_mode` (needs `index` + at least one of them). " +
14
+ 'remove: delete one filter from the stack (needs `index`). Removing a filter renumbers every index above it — re-run op=list before the next index-taking call.',
15
+ },
16
+ index: {
17
+ type: 'integer',
18
+ description: "1-based index of the filter to act on, as reported by op=list. 1 is the FIRST-APPLIED filter (bottom of the Smart Filters stack in the Layers panel). Required for every op except 'list'.",
19
+ minimum: 1,
20
+ maximum: 1000,
21
+ },
22
+ enabled: {
23
+ type: 'boolean',
24
+ description: 'set_visibility only: true shows the filter, false hides it. The filter stays in the stack either way and keeps all its settings.',
25
+ },
26
+ opacity: {
27
+ type: 'number',
28
+ description: 'set_blend only: filter opacity 0-100. Omit to leave the current opacity untouched.',
29
+ minimum: 0,
30
+ maximum: 100,
31
+ },
32
+ blend_mode: {
33
+ type: 'string',
34
+ enum: [...LAYER_BLEND_MODES],
35
+ description: 'set_blend only: how the filter result composites against the unfiltered layer. Same names as ps_set_layer. Omit to leave the current mode untouched.',
36
+ },
37
+ },
38
+ required: ['op'],
39
+ };
40
+ export async function runSmartFilterOp(connection, snippetClient, rawArgs) {
41
+ let args;
42
+ try {
43
+ args = validateArgs(smartFilterInputSchema, rawArgs);
44
+ }
45
+ catch (error) {
46
+ return toolErrorResult('Error in ps_filter', error);
47
+ }
48
+ const op = args.op;
49
+ if (op !== 'list' && args.index === undefined) {
50
+ return toolErrorResult('Error in ps_filter', new Error(`op=${op} needs an \`index\`. Call op=list first to see the filters and their indices.`));
51
+ }
52
+ switch (op) {
53
+ case 'list':
54
+ return runSnippetTool({
55
+ connection,
56
+ snippetClient,
57
+ rawArgs: args,
58
+ schema: smartFilterInputSchema,
59
+ snippet: 'listSmartFilters',
60
+ errorPrefix: 'Error listing Smart Filters',
61
+ successText: (result) => summarizeList(result),
62
+ });
63
+ case 'set_visibility': {
64
+ if (args.enabled === undefined) {
65
+ return toolErrorResult('Error in ps_filter', new Error('op=set_visibility needs `enabled` (true to show the filter, false to hide it).'));
66
+ }
67
+ return runSnippetTool({
68
+ connection,
69
+ snippetClient,
70
+ rawArgs: args,
71
+ schema: smartFilterInputSchema,
72
+ snippet: 'setSmartFilterVisibility',
73
+ errorPrefix: 'Error setting Smart Filter visibility',
74
+ params: (a) => ({ index: a.index, enabled: a.enabled }),
75
+ successText: (result) => {
76
+ const r = result;
77
+ return `${r.enabled ? 'Showed' : 'Hid'} Smart Filter ${String(r.index)} (${String(r.filter_name)}) on "${String(r.layer_name)}".`;
78
+ },
79
+ });
80
+ }
81
+ case 'set_blend': {
82
+ if (args.opacity === undefined && args.blend_mode === undefined) {
83
+ return toolErrorResult('Error in ps_filter', new Error('op=set_blend needs at least one of `opacity` or `blend_mode`.'));
84
+ }
85
+ return runSnippetTool({
86
+ connection,
87
+ snippetClient,
88
+ rawArgs: args,
89
+ schema: smartFilterInputSchema,
90
+ snippet: 'setSmartFilterBlend',
91
+ errorPrefix: 'Error setting Smart Filter blend',
92
+ params: (a) => {
93
+ const p = { index: a.index };
94
+ if (a.opacity !== undefined)
95
+ p.opacity = a.opacity;
96
+ if (a.blend_mode !== undefined)
97
+ p.blendMode = a.blend_mode;
98
+ return p;
99
+ },
100
+ successText: (result) => {
101
+ const r = result;
102
+ const opacity = Number(r.opacity);
103
+ const opacityText = Number.isFinite(opacity) ? opacity.toFixed(1) : String(r.opacity);
104
+ return `Smart Filter ${String(r.index)} (${String(r.filter_name)}) is now ${String(r.blend_mode)} at ${opacityText}% opacity on "${String(r.layer_name)}".`;
105
+ },
106
+ });
107
+ }
108
+ case 'remove':
109
+ return runSnippetTool({
110
+ connection,
111
+ snippetClient,
112
+ rawArgs: args,
113
+ schema: smartFilterInputSchema,
114
+ snippet: 'removeSmartFilter',
115
+ errorPrefix: 'Error removing Smart Filter',
116
+ params: (a) => ({ index: a.index }),
117
+ successText: (result) => {
118
+ const r = result;
119
+ return `Removed Smart Filter ${String(r.index)} (${String(r.removed_filter_name)}) from "${String(r.layer_name)}". ${String(r.remaining_count)} remaining. Indices renumbered — re-run op=list before the next index-taking call.`;
120
+ },
121
+ });
122
+ default:
123
+ return toolErrorResult('Error in ps_filter', new Error(`Unknown smart-filter op: ${String(op)}. Allowed: ${SMART_FILTER_OPS.join(', ')}.`));
124
+ }
125
+ }
126
+ function summarizeList(r) {
127
+ const layer = String(r.layer_name ?? '');
128
+ if (r.is_smart_object !== true) {
129
+ return `"${layer}" is not a Smart Object, so it has no Smart Filters. Convert it with ps_convert_to_smart_object to make filters re-editable.`;
130
+ }
131
+ const filters = Array.isArray(r.filters) ? r.filters : [];
132
+ if (filters.length === 0) {
133
+ return `"${layer}" is a Smart Object with no Smart Filters yet. Add one with ps_filter as_smart_filter=true.`;
134
+ }
135
+ const lines = filters.map((f) => {
136
+ const state = f.enabled === false ? ' [hidden]' : '';
137
+ const blend = f.blend_mode === 'NORMAL' && f.opacity === 100
138
+ ? ''
139
+ : ` — ${String(f.blend_mode)} @ ${String(f.opacity)}%`;
140
+ return ` ${String(f.index)}. ${String(f.name)} (${String(f.type)})${blend}${state}`;
141
+ });
142
+ return `"${layer}" has ${filters.length} Smart Filter${filters.length === 1 ? '' : 's'} (1 = first applied, bottom of the stack):\n${lines.join('\n')}`;
143
+ }
144
+ export async function getSmartObjectInfoHandler(connection, snippetClient) {
145
+ return runSnippetTool({
146
+ connection,
147
+ snippetClient,
148
+ rawArgs: {},
149
+ schema: { type: 'object', properties: {} },
150
+ snippet: 'getSmartObjectInfo',
151
+ errorPrefix: 'Error reading Smart Object info',
152
+ successText: (result) => {
153
+ const r = result;
154
+ const layer = String(r.layer_name ?? '');
155
+ if (r.is_smart_object !== true) {
156
+ return `"${layer}" is not a Smart Object (kind: ${String(r.layer_kind)}).`;
157
+ }
158
+ const storage = r.linked === true ? `linked to ${String(r.file_reference)}` : 'embedded';
159
+ const n = Number(r.smart_filter_count ?? 0);
160
+ return `"${layer}" is a ${storage} Smart Object with ${n} Smart Filter${n === 1 ? '' : 's'}.`;
161
+ },
162
+ });
163
+ }
@@ -1,6 +1,7 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
- import { toolErrorResult, runSnippetTool } from '../utils/tool-helpers.js';
3
+ import { toolErrorResult, runSnippetTool, unknownDiscriminator } from '../utils/tool-helpers.js';
4
+ import { createTextLayer, createTextLayerSchema } from './layer-tools.js';
4
5
  const setTextFontSchema = {
5
6
  type: 'object',
6
7
  properties: {
@@ -87,12 +88,83 @@ const SET_TEXT_INPUT_SCHEMA = {
87
88
  },
88
89
  required: ['property'],
89
90
  };
91
+ const TEXT_OPS = ['create', 'set_content', 'set_font', 'set_color', 'set_alignment'];
92
+ export const TEXT_OP_SCHEMAS = {
93
+ create: createTextLayerSchema,
94
+ set_content: updateTextContentSchema,
95
+ set_font: setTextFontSchema,
96
+ set_color: setTextColorSchema,
97
+ set_alignment: setTextAlignmentSchema,
98
+ };
99
+ const TEXT_INPUT_SCHEMA = {
100
+ type: 'object',
101
+ properties: {
102
+ op: {
103
+ type: 'string',
104
+ enum: [...TEXT_OPS],
105
+ description: 'create: new text layer with `text` at `x`/`y` (default 100,100) and `font_size` (default 24). ' +
106
+ "set_content: replace the active text layer's content with `text`. " +
107
+ "set_font: set the active text layer's font to `font_name` (optionally `font_size`). " +
108
+ "set_color: set the active text layer's color to `red`/`green`/`blue`. " +
109
+ "set_alignment: set the active text layer's `alignment`.",
110
+ },
111
+ ...createTextLayerSchema.properties,
112
+ ...setTextColorSchema.properties,
113
+ ...setTextAlignmentSchema.properties,
114
+ font_name: setTextFontSchema.properties.font_name,
115
+ text: {
116
+ type: 'string',
117
+ description: 'create: initial text content. set_content: new text content, replacing the existing text.',
118
+ },
119
+ font_size: {
120
+ type: 'number',
121
+ description: "Font size in points, up to 1296 — Photoshop's own ceiling for the Character panel. create: initial size (default 24). set_font: new size (optional — omit to leave the current size unchanged).",
122
+ default: 24,
123
+ minimum: 1,
124
+ maximum: 1296,
125
+ },
126
+ },
127
+ required: ['op'],
128
+ };
90
129
  export function createTextTools(connection, snippetClient) {
91
130
  return [
131
+ {
132
+ tool: {
133
+ name: 'ps_text',
134
+ description: 'Text layer — create it or style it, chosen with `op`. create: a new text layer with `text` at `x`/`y` and `font_size`. set_content/set_font/set_color/set_alignment act on the currently active text layer (throws if it isn\'t one). Font names accept either the PostScript name ("ArialMT") or family name ("Arial", resolved to its Regular/first variant); throws clearly if no installed font matches. The set_* ops are idempotent.',
135
+ inputSchema: TEXT_INPUT_SCHEMA,
136
+ outputSchema: {
137
+ type: 'object',
138
+ properties: {
139
+ created: { type: 'boolean', description: 'op=create: true on success.' },
140
+ layerName: { type: 'string', description: 'op=create: the new layer name.' },
141
+ position: { type: 'object', description: 'op=create: {x, y}.' },
142
+ fontSize: { type: 'number', description: 'op=create: the size applied.' },
143
+ requested: { type: 'string', description: 'op=set_font: the font requested.' },
144
+ font: { type: 'string', description: 'op=set_font: the font actually matched.' },
145
+ size: { type: 'number', description: 'op=set_font: the size applied, if given.' },
146
+ matched_by: {
147
+ type: 'string',
148
+ enum: ['postScriptName', 'family+regular', 'family', 'name'],
149
+ description: 'op=set_font: how `font_name` was resolved.',
150
+ },
151
+ color: { type: 'string', description: 'op=set_color: the RGB() string applied.' },
152
+ alignment: { type: 'string', description: 'op=set_alignment: the alignment applied.' },
153
+ text: { type: 'string', description: 'op=create/set_content: the text content.' },
154
+ context: { type: 'object' },
155
+ },
156
+ },
157
+ annotations: {
158
+ title: 'Text',
159
+ idempotentHint: false,
160
+ },
161
+ },
162
+ handler: async (args) => textDispatch(connection, snippetClient, args),
163
+ },
92
164
  {
93
165
  tool: {
94
166
  name: 'ps_set_text',
95
- description: 'Set an attribute of the currently active text layer — font (family or PostScript name, optionally size), color (RGB), alignment, or text content — selected via `property`. Idempotent. Throws if the active layer is not a text layer. Font names accept either the PostScript name ("ArialMT") or family name ("Arial", resolved to its Regular/first variant); throws clearly if no installed font matches.',
167
+ description: 'DEPRECATED — use ps_text(op=set_font / set_color / set_alignment / set_content) instead (kept for one release for backward compatibility, identical behaviour). Set an attribute of the currently active text layer — font (family or PostScript name, optionally size), color (RGB), alignment, or text content — selected via `property`. Idempotent. Throws if the active layer is not a text layer. Font names accept either the PostScript name ("ArialMT") or family name ("Arial", resolved to its Regular/first variant); throws clearly if no installed font matches.',
96
168
  inputSchema: SET_TEXT_INPUT_SCHEMA,
97
169
  outputSchema: {
98
170
  type: 'object',
@@ -118,6 +190,24 @@ export function createTextTools(connection, snippetClient) {
118
190
  },
119
191
  ];
120
192
  }
193
+ async function textDispatch(connection, snippetClient, rawArgs) {
194
+ const op = rawArgs.op;
195
+ const { op: _omit, ...rest } = rawArgs;
196
+ switch (op) {
197
+ case 'create':
198
+ return createTextLayer(connection, snippetClient, rest);
199
+ case 'set_content':
200
+ return updateTextContent(connection, snippetClient, rest);
201
+ case 'set_font':
202
+ return setTextFont(connection, snippetClient, rest);
203
+ case 'set_color':
204
+ return setTextColor(connection, snippetClient, rest);
205
+ case 'set_alignment':
206
+ return setTextAlignment(connection, snippetClient, rest);
207
+ default:
208
+ return unknownDiscriminator('text op', op, TEXT_OPS);
209
+ }
210
+ }
121
211
  async function setText(connection, snippetClient, rawArgs) {
122
212
  const property = rawArgs.property;
123
213
  const { property: _omit, ...rest } = rawArgs;
@@ -23,21 +23,57 @@ 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
+ {
28
+ errorClass: 'layer_not_found',
29
+ pattern: /\blayer not found|no layer named|(?:layer_to_move|target_layer_name) not found|layer "[^"]*" not found/i,
30
+ },
31
+ { errorClass: 'group_not_found', pattern: /group not found/i },
32
+ { errorClass: 'channel_not_found', pattern: /channel not found|channel named/i },
33
+ { errorClass: 'path_not_found', pattern: /no path named|no paths to|no work path/i },
34
+ { errorClass: 'font_not_found', pattern: /font not found/i },
35
+ {
36
+ errorClass: 'file_not_found',
37
+ pattern: /file not found|map not found|lut not found|could not open lut/i,
38
+ },
39
+ { errorClass: 'face_not_found', pattern: /no face mesh|no face detected/i },
26
40
  {
27
41
  errorClass: 'schema_validation',
28
- pattern: /\bvalidat|required.*field|must be.*type|invalid (input|argument)/i,
42
+ pattern: /\bvalidat|required.*field|missing required argument|must be.*type|invalid (input|argument)/i,
29
43
  },
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
44
  {
34
- errorClass: 'ps_modal_blocking',
35
- pattern: /modal.*dialog|dialog.*blocking|blocked.*modal|photoshop.*modal/i,
45
+ errorClass: 'invalid_argument',
46
+ pattern: /unknown [^:]{1,30}:|invalid |illegal argument|must be |out of bounds|unsupported/i,
36
47
  },
48
+ { errorClass: 'ps_not_detected', pattern: /photoshop info not available/i },
37
49
  {
38
50
  errorClass: 'ps_not_running',
39
51
  pattern: /CreateObject|photoshop.*not.*running|cannot connect.*photoshop|connection.*failed/i,
40
52
  },
53
+ { errorClass: 'no_document', pattern: /no active document|no document is open/i },
54
+ { errorClass: 'no_active_layer', pattern: /no active layer|document has no layers/i },
55
+ {
56
+ errorClass: 'no_selection',
57
+ pattern: /no active selection|requires an active selection|make a selection/i,
58
+ },
59
+ { errorClass: 'background_layer', pattern: /background layer/i },
60
+ { errorClass: 'layer_locked', pattern: /is locked|fully locked|locked layer/i },
61
+ {
62
+ errorClass: 'wrong_layer_kind',
63
+ pattern: /pixel layer|text layer|smart object layer|layer kind|rasterize it first/i,
64
+ },
65
+ { errorClass: 'ps_command_unavailable', pattern: /not currently available/i },
66
+ { errorClass: 'timeout', pattern: /timed? ?out|Script execution timeout|exceeded.*bytes/i },
67
+ {
68
+ errorClass: 'ps_modal_blocking',
69
+ pattern: /modal.*dialog|dialog.*blocking|blocked.*modal|photoshop.*modal/i,
70
+ },
71
+ { errorClass: 'ai_selection_no_result', pattern: /returned no result/i },
72
+ { errorClass: 'ps_general_error', pattern: /general photoshop error/i },
73
+ { errorClass: 'ps_no_such_element', pattern: /no such element/i },
74
+ { errorClass: 'write_not_verified', pattern: /did not verify/i },
75
+ { errorClass: 'ps_empty_error', pattern: /:\s*$/ },
76
+ { errorClass: 'ps_op_failed', pattern: /failed[: (]/i },
41
77
  ];
42
78
  export function classifyError(error) {
43
79
  if (error === undefined)
@@ -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.2';
1
+ export const VERSION = '1.1.0';
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "editmamei",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Photoshop MCP server: natural-language AI photo editing with your own Photoshop (Community Edition)",
5
+ "mcpName": "io.github.editmamei/editmamei",
5
6
  "main": "dist/index.js",
6
7
  "type": "module",
7
8
  "bin": {
@@ -44,7 +45,7 @@
44
45
  "url": "https://github.com/editmamei/editmamei/issues"
45
46
  },
46
47
  "engines": {
47
- "node": ">=20.0.0"
48
+ "node": ">=22.0.0"
48
49
  },
49
50
  "dependencies": {
50
51
  "@modelcontextprotocol/sdk": "^1.0.4",