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
package/README.md CHANGED
@@ -47,7 +47,7 @@ Then restart your AI client and ask it: _"Is Photoshop connected?"_
47
47
 
48
48
  Prefer not to use a terminal? Claude Desktop users can install the one-click [`.mcpb` extension](https://github.com/editmamei/editmamei/releases/latest/download/editmamei.mcpb) directly (no system Node required, Claude Desktop ships its own runtime). Full walkthrough in the [install guide](docs/installation.md).
49
49
 
50
- **Requirements:** Adobe Photoshop 2026 (v27.x), Windows 10/11 or macOS 12+, Node.js 20+ (for the npm path), and an MCP-compatible AI client (Claude Desktop, Cursor, Claude Code).
50
+ **Requirements:** Adobe Photoshop 2026 (v27.x), Windows 10/11 or macOS 12+, Node.js 22+ (for the npm path), and an MCP-compatible AI client (Claude Desktop, Cursor, Claude Code).
51
51
 
52
52
  ## A session, end to end
53
53
 
@@ -75,7 +75,7 @@ These are the choices that shape the surface, and the reason an AI assistant can
75
75
 
76
76
  ## Tool surface
77
77
 
78
- **87 tools across 16 capability groups** (64 Community, 23 Pro). Every tool is namespaced `ps_*` and discoverable at runtime via `tools/list`. Community tools ship in both editions; Pro tools unlock with a license.
78
+ **82 tools across 16 capability groups** (59 Community, 23 Pro). Every tool is namespaced `ps_*` and discoverable at runtime via `tools/list`. Community tools ship in both editions; Pro tools unlock with a license. `tools/list` reports eight more than the table below: the names superseded by `filter`, `group`, and `text` (`apply_filter`, `create_group`, `move_layer_to_group`, `set_group_blend_mode`, `ungroup`, `delete_group`, `create_text_layer`, `set_text`) stay registered as deprecated aliases for one release, so existing calls keep working.
79
79
 
80
80
  | Group | Edition | Tools |
81
81
  | --- | --- | --- |
@@ -85,11 +85,11 @@ These are the choices that shape the surface, and the reason an AI assistant can
85
85
  | **Document & canvas** | Community | `create_document` · `open_document` · `close_document` · `save_psd` · `export` · `place_image` · `resize_image` · `crop_document` · `convert_image_mode` · `transform_canvas` · `guides` |
86
86
  | **Selection** | Community | `select` · `modify_selection` · `selection_channel` |
87
87
  | **Adjustments** | Community | `add_adjustment_layer` · `apply_adjustment` |
88
- | **Filters** | Community | `apply_filter` |
88
+ | **Filters** | Community | `filter` |
89
89
  | **Retouch** | Community | `retouch` |
90
- | **Layers** | Community | `create_layer` · `delete_layer` · `fill_layer` · `add_fill_layer` · `select_layer` · `move_layer_to_position` · `duplicate_layer` · `copy_to_new_layer` · `convert_to_smart_object` · `rasterize_layer` · `set_layer` · `merge` · `bake_layer` · `add_layer_style` · `transform_layer` · `shape` · `create_group` · `move_layer_to_group` · `set_group_blend_mode` · `ungroup` · `delete_group` |
91
- | **Masks & paths** | Community | `layer_mask` · `create_clipping_mask` · `path` · `vector_mask` · `apply_image` · `calculations` |
92
- | **Type** | Community | `create_text_layer` · `set_text` |
90
+ | **Layers** | Community | `create_layer` · `delete_layer` · `fill_layer` · `add_fill_layer` · `select_layer` · `move_layer_to_position` · `duplicate_layer` · `copy_to_new_layer` · `convert_to_smart_object` · `rasterize_layer` · `set_layer` · `merge` · `bake_layer` · `add_layer_style` · `transform_layer` · `shape` · `group` |
91
+ | **Masks & paths** | Community | `layer_mask` · `clipping_mask` · `path` · `vector_mask` · `apply_image` · `calculations` |
92
+ | **Type** | Community | `text` |
93
93
  | **Perception** | Community | `detect` · `read_scene` · `select_by_reference` |
94
94
  | **AI selection** | Community | `select_subject` · `select_sky` |
95
95
  | **AI selection** | Pro | `select_subject_instance` · `select_object` |
@@ -119,7 +119,7 @@ npm run build
119
119
  npm test
120
120
  ```
121
121
 
122
- You need Node.js 20+ and a Go toolchain (the build compiles the `editmamei-core` binary that generates Photoshop scripts). The build warns instead of failing when Go is missing, so you can still run the test suite; set `EDITMAMEI_CORE_BIN` to a prebuilt binary if you would rather not install Go.
122
+ You need Node.js 22+ and a Go toolchain (the build compiles the `editmamei-core` binary that generates Photoshop scripts). The build warns instead of failing when Go is missing, so you can still run the test suite; set `EDITMAMEI_CORE_BIN` to a prebuilt binary if you would rather not install Go.
123
123
 
124
124
  The test suite runs without Photoshop. It verifies the ExtendScript Editmamei generates, never that Photoshop accepted it, so live verification against a real Photoshop is a separate step.
125
125
 
@@ -18,6 +18,69 @@ function normName(s) {
18
18
  return result.replace(/\\s+/g, ' ').replace(/^\\s+|\\s+$/g, '').toLowerCase();
19
19
  }
20
20
  `;
21
+ export const notFoundMessageHelper = `
22
+ function __notFoundMessage(label, requested, groupsOnly) {
23
+ var kept = [];
24
+ var total = 0;
25
+ var walkBroke = false;
26
+ function __nfEsc(s) {
27
+ var out = '';
28
+ for (var c = 0; c < s.length; c++) {
29
+ var code = s.charCodeAt(c);
30
+ if (code >= 32 && code <= 126 && code !== 92) {
31
+ out += s.charAt(c);
32
+ } else {
33
+ var hex = code.toString(16);
34
+ while (hex.length < 4) hex = '0' + hex;
35
+ out += '\\\\u' + hex;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ function consider(layer) {
41
+ var nm = '';
42
+ try { nm = String(layer.name); } catch (eN) { walkBroke = true; return; }
43
+ total++;
44
+ if (kept.length >= 8) return;
45
+ nm = __nfEsc(nm);
46
+ if (nm.length > 40) {
47
+ nm = nm.substring(0, 40);
48
+ var cut = nm.lastIndexOf('\\\\u');
49
+ if (cut > 34) nm = nm.substring(0, cut);
50
+ nm = nm + '...';
51
+ }
52
+ kept.push(nm);
53
+ }
54
+ function walk(layers, depth) {
55
+ var n = 0;
56
+ try { n = layers.length; } catch (eL) { walkBroke = true; return; }
57
+ for (var i = 0; i < n; i++) {
58
+ var l = null;
59
+ try { l = layers[i]; } catch (eI) { walkBroke = true; continue; }
60
+ if (!l) continue;
61
+ var isGroup = false;
62
+ try { isGroup = (l instanceof LayerSet); } catch (eG) {}
63
+ if (isGroup || !groupsOnly) consider(l);
64
+ if (isGroup && depth < 32) {
65
+ try { walk(l.layers, depth + 1); } catch (eD) { walkBroke = true; }
66
+ }
67
+ }
68
+ }
69
+ try { walk(app.activeDocument.layers, 0); } catch (eW) { walkBroke = true; }
70
+ if (walkBroke && total === 0) {
71
+ return label + ' not found: ' + __nfEsc(String(requested));
72
+ }
73
+ var have;
74
+ if (total === 0) {
75
+ have = groupsOnly ? '(no groups)' : '(none)';
76
+ } else {
77
+ have = kept.join(', ');
78
+ if (total > kept.length) have += ' (+' + (total - kept.length) + ' more)';
79
+ if (walkBroke) have += ' (list may be incomplete)';
80
+ }
81
+ return label + ' not found: ' + __nfEsc(String(requested)) + '. Have: ' + have;
82
+ }
83
+ `;
21
84
  export const selectionTypeHelpers = `
22
85
  function mapSelType(s) {
23
86
  if (s === 'add' || s === 'extend') return SelectionType.EXTEND;
@@ -222,8 +285,7 @@ function getContextInfo() {
222
285
  // during the context probe should NOT make the op look failed to the
223
286
  // caller. On any unexpected throw we return a degraded context with
224
287
  // an error_reading field so the caller can see something happened
225
- // but the structured result still arrives. T05 P1-4 in the
226
- // launch-readiness review.
288
+ // but the structured result still arrives.
227
289
  try {
228
290
  var context = {
229
291
  hasDocument: app.documents.length > 0
@@ -33,7 +33,7 @@ class ExtendScriptPhotoshopAPI {
33
33
  'This usually means PS is in a stuck/modal state (a leaked preview duplicate ' +
34
34
  'or a pending dialog from a prior timeout), or there is no active document. ' +
35
35
  'Check that a document is open and try once more; if it persists, dismiss any ' +
36
- 'open Photoshop dialog.');
36
+ 'open Photoshop dialog.', { cause: err });
37
37
  }
38
38
  throw err instanceof Error ? err : new Error(msg);
39
39
  }
Binary file
Binary file
@@ -33,7 +33,7 @@ export async function runActivate(opts = {}) {
33
33
  catch (e) {
34
34
  if (e instanceof PolarLicenseError) {
35
35
  err(`Activation failed: ${e.message}\n`);
36
- throw new Error(e.code);
36
+ throw new Error(e.code, { cause: e });
37
37
  }
38
38
  throw e;
39
39
  }
@@ -98,7 +98,7 @@ export function runConfig(args, io = {}) {
98
98
  }
99
99
  catch (e) {
100
100
  err(`Invalid value for ${key}: ${e instanceof Error ? e.message : String(e)}\n`);
101
- throw new Error('config set: bad value');
101
+ throw new Error('config set: bad value', { cause: e });
102
102
  }
103
103
  const { settings } = loadSettings(io);
104
104
  spec.set(settings, coerced);
@@ -12,7 +12,7 @@ export async function runDeactivate(opts = {}) {
12
12
  catch (e) {
13
13
  if (e instanceof PolarLicenseError) {
14
14
  err(`Deactivate failed: ${e.message}\n`);
15
- throw new Error(e.code);
15
+ throw new Error(e.code, { cause: e });
16
16
  }
17
17
  throw e;
18
18
  }
@@ -0,0 +1,13 @@
1
+ let pending = null;
2
+ export function markRawOpened(documentName, filePath) {
3
+ pending = { documentName, filePath };
4
+ }
5
+ export function clearPendingRawDevelop() {
6
+ pending = null;
7
+ }
8
+ export function getPendingRawDevelop() {
9
+ return pending;
10
+ }
11
+ export function __clearRawDevelopState() {
12
+ pending = null;
13
+ }
@@ -21,6 +21,7 @@ import { runScript } from '../utils/run-script.js';
21
21
  import { listTemplates } from '../utils/template-storage.js';
22
22
  import { toolErrorResult } from '../utils/tool-helpers.js';
23
23
  import { Kernel } from '../kernel/kernel.js';
24
+ import { markRawOpened, clearPendingRawDevelop } from './raw-develop-state.js';
24
25
  import { ceModule } from '../modules/ce/index.js';
25
26
  import { hostDetectionRuntime } from '../detection/runtime.js';
26
27
  import { ModuleLifecycle, classifyModuleOutcome } from '../kernel/module-lifecycle.js';
@@ -45,6 +46,7 @@ const FIRST_RUN_DISCLOSURE = 'First run: Editmamei collects anonymous, content-f
45
46
  'It never sends image content, file paths, or personal data. Opt out anytime: ' +
46
47
  '`editmamei config set telemetry.usage false` (or edit ~/.editmamei/settings.json). ' +
47
48
  'Opt in to sanitized diagnostics: `editmamei config set telemetry.diagnostics true`.';
49
+ export const BACKGROUND_VERSION_PROBE_TIMEOUT_MS = 3_000;
48
50
  export class EditmameiServer {
49
51
  server;
50
52
  logger;
@@ -58,6 +60,8 @@ export class EditmameiServer {
58
60
  return this.moduleLifecycle.moduleSkipReason;
59
61
  }
60
62
  psVersion = null;
63
+ lastPingReachedPs = null;
64
+ liveVersionResolutionAttempted = false;
61
65
  updateInfo = null;
62
66
  snippetClient = new GoSnippetClient();
63
67
  refreshLicenseOnPing = createPingLicenseRefresher();
@@ -85,6 +89,11 @@ export class EditmameiServer {
85
89
  warnLogScriptOnErrorOnce(this.logger);
86
90
  this.toolRegistry = new ToolRegistry({
87
91
  onCall: (entry) => {
92
+ if (entry.success && entry.tool !== 'ps_ping' && this.psVersion === null) {
93
+ if (this.session.getConnection().hasReachedPhotoshop()) {
94
+ this.resolveLiveVersionInBackground();
95
+ }
96
+ }
88
97
  void this.sessionLog.append({
89
98
  tool: entry.tool,
90
99
  args: entry.args,
@@ -92,10 +101,11 @@ export class EditmameiServer {
92
101
  duration_ms: entry.duration_ms,
93
102
  ...(entry.error ? { error: entry.error } : {}),
94
103
  }, entry.result);
104
+ const telemetrySuccess = entry.tool === 'ps_ping' && this.lastPingReachedPs === false ? false : entry.success;
95
105
  const errorClass = classifyError(entry.error);
96
106
  this.telemetry.recordCall({
97
107
  tool: entry.tool,
98
- success: entry.success,
108
+ success: telemetrySuccess,
99
109
  duration_ms: entry.duration_ms,
100
110
  error_class: errorClass,
101
111
  });
@@ -284,6 +294,7 @@ export class EditmameiServer {
284
294
  try {
285
295
  const result = await this.toolRegistry.execute(name, args);
286
296
  this.session.updateActivity();
297
+ this.trackRawDevelopState(name, result);
287
298
  return result;
288
299
  }
289
300
  catch (error) {
@@ -291,13 +302,56 @@ export class EditmameiServer {
291
302
  return toolErrorResult('Error', error);
292
303
  }
293
304
  }
305
+ trackRawDevelopState(name, result) {
306
+ if (result.isError)
307
+ return;
308
+ if (name === 'ps_open_document') {
309
+ const sc = result.structuredContent;
310
+ if (sc?.is_raw_source === true && this.toolRegistry.get('ps_apply_camera_raw')) {
311
+ const opened = sc;
312
+ markRawOpened(String(opened.document_name ?? ''), String(opened.file_path ?? ''));
313
+ }
314
+ else {
315
+ clearPendingRawDevelop();
316
+ }
317
+ }
318
+ else if (name === 'ps_apply_camera_raw' || name === 'ps_close_document') {
319
+ clearPendingRawDevelop();
320
+ }
321
+ }
322
+ resolveLiveVersionInBackground() {
323
+ if (this.liveVersionResolutionAttempted || this.psVersion !== null)
324
+ return;
325
+ void (async () => {
326
+ try {
327
+ const connection = this.session.getConnection();
328
+ const snippet = await this.snippetClient.build('pingState');
329
+ if (!(await connection.isCurrentlyRunning()))
330
+ return;
331
+ if (this.liveVersionResolutionAttempted || this.psVersion !== null)
332
+ return;
333
+ this.liveVersionResolutionAttempted = true;
334
+ const state = (await runScript(connection, snippet, BACKGROUND_VERSION_PROBE_TIMEOUT_MS));
335
+ if (state.version && state.version !== 'Unknown' && this.psVersion === null) {
336
+ this.psVersion = state.version;
337
+ void this.sessionLog.setPsVersion(state.version);
338
+ this.telemetry.onPsVersionResolved();
339
+ }
340
+ }
341
+ catch (err) {
342
+ this.logger.debug(`background ps_version resolution failed: ${err instanceof Error ? err.message : String(err)}`);
343
+ }
344
+ })();
345
+ }
294
346
  async pingPhotoshop() {
347
+ this.lastPingReachedPs = null;
295
348
  this.refreshLicenseOnPing();
296
349
  const connection = this.session.getConnection();
297
350
  let version = 'Unknown';
298
351
  let actionSetsCount = 0;
299
352
  let openDocuments = [];
300
353
  const degraded = [];
354
+ let versionIsLive = false;
301
355
  try {
302
356
  const detected = await connection.getVersion();
303
357
  if (detected)
@@ -314,6 +368,7 @@ export class EditmameiServer {
314
368
  const alive = await connection.ping().catch(() => false);
315
369
  if (!alive) {
316
370
  const reason = unsupportedHostReason();
371
+ this.lastPingReachedPs = false;
317
372
  return {
318
373
  content: [
319
374
  {
@@ -333,6 +388,7 @@ export class EditmameiServer {
333
388
  }
334
389
  catch (err) {
335
390
  this.logger.warn(`pingState snippet failed: ${err instanceof Error ? err.message : String(err)}`);
391
+ this.lastPingReachedPs = false;
336
392
  return {
337
393
  content: [
338
394
  { type: 'text', text: 'Photoshop did not respond' + this.updateNote() },
@@ -340,8 +396,10 @@ export class EditmameiServer {
340
396
  structuredContent: { connected: false, update_available: this.updateInfo },
341
397
  };
342
398
  }
343
- if (state.version)
399
+ if (state.version) {
344
400
  version = state.version;
401
+ versionIsLive = true;
402
+ }
345
403
  if (typeof state.action_sets_count === 'number')
346
404
  actionSetsCount = state.action_sets_count;
347
405
  if (Array.isArray(state.open_documents))
@@ -357,10 +415,13 @@ export class EditmameiServer {
357
415
  degraded.push('templates');
358
416
  }
359
417
  if (version !== 'Unknown') {
360
- this.psVersion = version;
361
418
  void this.sessionLog.setPsVersion(version);
419
+ }
420
+ if (versionIsLive && version !== 'Unknown') {
421
+ this.psVersion = version;
362
422
  this.telemetry.onPsVersionResolved();
363
423
  }
424
+ this.lastPingReachedPs = true;
364
425
  const degradedNote = degraded.length ? ` (degraded: ${degraded.join(', ')})` : '';
365
426
  return {
366
427
  content: [
@@ -7,12 +7,12 @@ export const GROUPS = {
7
7
  inspect: {
8
8
  id: 'inspect',
9
9
  label: 'Inspect',
10
- purpose: 'Read document/layer/selection/history state and render previews.',
10
+ purpose: 'Read document/layer/selection/history/smart-object state and render previews.',
11
11
  },
12
12
  verify: {
13
13
  id: 'verify',
14
14
  label: 'Verify',
15
- purpose: 'Measurement primitives — histogram, region compare, bounds diff — to check work instead of eyeballing.',
15
+ purpose: 'Measurement primitives — histogram, region compare, bounds diff, selection preview — to check work instead of eyeballing.',
16
16
  },
17
17
  document: {
18
18
  id: 'document',
@@ -22,12 +22,12 @@ export const GROUPS = {
22
22
  select: {
23
23
  id: 'select',
24
24
  label: 'Selection',
25
- purpose: 'Manual selections, selection edits, and alpha-channel save/load.',
25
+ purpose: 'Manual selections, selection edits, and alpha-channel save/load/duplicate/delete.',
26
26
  },
27
27
  select_ai: {
28
28
  id: 'select_ai',
29
29
  label: 'AI selection',
30
- purpose: 'Sensei/CV-backed subject & sky selection (Pro).',
30
+ purpose: 'Sensei/CV-backed selection: select_subject and select_sky (community), plus subject-instance selection and named-object selection (Pro).',
31
31
  },
32
32
  adjust: {
33
33
  id: 'adjust',
@@ -37,12 +37,12 @@ export const GROUPS = {
37
37
  filter: {
38
38
  id: 'filter',
39
39
  label: 'Filters',
40
- purpose: 'Blur/sharpen/noise/stylize filters (auto-duplicate-first by default).',
40
+ purpose: 'Blur/sharpen/noise/stylize filters (auto-duplicate-first by default) and the Camera Raw develop surface, plus managing the re-editable Smart Filter stack on a Smart Object.',
41
41
  },
42
42
  retouch: {
43
43
  id: 'retouch',
44
44
  label: 'Retouch',
45
- purpose: 'Content-aware fill/patch/move and brush strokes.',
45
+ purpose: 'Content-aware fill, patch, and move.',
46
46
  },
47
47
  layers: {
48
48
  id: 'layers',
@@ -52,7 +52,7 @@ export const GROUPS = {
52
52
  masks: {
53
53
  id: 'masks',
54
54
  label: 'Masks & paths',
55
- purpose: 'Layer masks, clipping masks, vector masks, and work paths.',
55
+ purpose: 'Layer masks, clipping masks, vector masks, work paths, and channel compositing (apply image / calculations).',
56
56
  },
57
57
  type: {
58
58
  id: 'type',
@@ -66,8 +66,8 @@ export const GROUPS = {
66
66
  },
67
67
  face: {
68
68
  id: 'face',
69
- label: 'Face mesh (Pro)',
70
- purpose: 'Face-landmark perception and mesh-aimed feature selection / contour strokes (Pro).',
69
+ label: 'Face mesh',
70
+ purpose: 'Face-landmark perception and mesh-aimed feature selection / contour strokes. Pro in shipped builds.',
71
71
  },
72
72
  templates: {
73
73
  id: 'templates',
@@ -113,6 +113,7 @@ export const TOOL_GROUPS = {
113
113
  ps_select_object: 'select_ai',
114
114
  ps_add_adjustment_layer: 'adjust',
115
115
  ps_apply_adjustment: 'adjust',
116
+ ps_filter: 'filter',
116
117
  ps_apply_filter: 'filter',
117
118
  ps_apply_camera_raw: 'filter',
118
119
  ps_retouch: 'retouch',
@@ -137,19 +138,20 @@ export const TOOL_GROUPS = {
137
138
  ps_warp_layer_along: 'layers',
138
139
  ps_warp_layer_region: 'layers',
139
140
  ps_warp_layer_to: 'layers',
141
+ ps_group: 'layers',
140
142
  ps_create_group: 'layers',
141
143
  ps_move_layer_to_group: 'layers',
142
144
  ps_set_group_blend_mode: 'layers',
143
145
  ps_ungroup: 'layers',
144
146
  ps_delete_group: 'layers',
147
+ ps_shape: 'layers',
145
148
  ps_layer_mask: 'masks',
146
- ps_create_clipping_mask: 'masks',
147
- ps_release_clipping_mask: 'masks',
149
+ ps_clipping_mask: 'masks',
148
150
  ps_path: 'masks',
149
151
  ps_vector_mask: 'masks',
150
152
  ps_apply_image: 'masks',
151
153
  ps_calculations: 'masks',
152
- ps_shape: 'layers',
154
+ ps_text: 'type',
153
155
  ps_create_text_layer: 'type',
154
156
  ps_set_text: 'type',
155
157
  ps_detect: 'perception',
@@ -12,14 +12,15 @@ export const TOOL_TIERS = {
12
12
  ps_open_document: 'community',
13
13
  ps_save_psd: 'community',
14
14
  ps_export: 'community',
15
+ ps_filter: 'community',
15
16
  ps_apply_filter: 'community',
17
+ ps_group: 'community',
16
18
  ps_create_group: 'community',
17
19
  ps_move_layer_to_group: 'community',
18
20
  ps_set_group_blend_mode: 'community',
19
21
  ps_ungroup: 'community',
20
22
  ps_delete_group: 'community',
21
- ps_create_clipping_mask: 'community',
22
- ps_release_clipping_mask: 'dev',
23
+ ps_clipping_mask: 'community',
23
24
  ps_undo: 'community',
24
25
  ps_redo: 'community',
25
26
  ps_place_image: 'community',
@@ -89,6 +90,7 @@ export const TOOL_TIERS = {
89
90
  ps_template_verify: 'pro',
90
91
  ps_template_recall: 'pro',
91
92
  ps_template_delete: 'pro',
93
+ ps_text: 'community',
92
94
  ps_set_text: 'community',
93
95
  };
94
96
  export function tierOf(toolName) {
@@ -67,7 +67,7 @@ export function decodeJpegBuffer(bytes) {
67
67
  }
68
68
  catch (err) {
69
69
  const msg = err instanceof Error ? err.message : String(err);
70
- throw new Error(`failed to decode JPEG buffer: ${msg}`);
70
+ throw new Error(`failed to decode JPEG buffer: ${msg}`, { cause: err });
71
71
  }
72
72
  }
73
73
  export function decodeJpeg(path) {
@@ -76,6 +76,6 @@ export function decodeJpeg(path) {
76
76
  }
77
77
  catch (err) {
78
78
  const msg = err instanceof Error ? err.message : String(err);
79
- throw new Error(`failed to decode JPEG at ${path}: ${msg}`);
79
+ throw new Error(`failed to decode JPEG at ${path}: ${msg}`, { cause: err });
80
80
  }
81
81
  }
@@ -81,7 +81,7 @@ export function scoreRegion(kind, signals, ctx) {
81
81
  signals,
82
82
  };
83
83
  }
84
- let confidence = 0;
84
+ let confidence;
85
85
  switch (kind) {
86
86
  case 'sky': {
87
87
  const upper = rampDown(signals.centroidY, 0.35, 0.65);
@@ -13,6 +13,7 @@ export class PhotoshopConnection {
13
13
  photoshopInfo = null;
14
14
  detectInFlight = null;
15
15
  runningLatchUntil = 0;
16
+ everReachedPhotoshop = false;
16
17
  constructor(opts = {}) {
17
18
  this.logger = new Logger('PhotoshopConnection');
18
19
  this.now = opts.now ?? Date.now;
@@ -71,6 +72,7 @@ export class PhotoshopConnection {
71
72
  }
72
73
  const result = await this.host.adapter.run(script, timeout);
73
74
  this.runningLatchUntil = this.now() + RUNNING_LATCH_TTL_MS;
75
+ this.everReachedPhotoshop = true;
74
76
  return result;
75
77
  }
76
78
  catch (error) {
@@ -99,4 +101,17 @@ export class PhotoshopConnection {
99
101
  getPhotoshopInfo() {
100
102
  return this.photoshopInfo;
101
103
  }
104
+ hasReachedPhotoshop() {
105
+ return this.everReachedPhotoshop;
106
+ }
107
+ async isCurrentlyRunning() {
108
+ if (!this.photoshopInfo)
109
+ return false;
110
+ try {
111
+ return await this.host.adapter.isRunning();
112
+ }
113
+ catch {
114
+ return false;
115
+ }
116
+ }
102
117
  }
Binary file
@@ -118,6 +118,19 @@ export class TelemetryClient {
118
118
  ...(diag.stderr_tail ? { stderr_tail: sanitizeStderrTail(diag.stderr_tail) } : {}),
119
119
  }, this.now()));
120
120
  }
121
+ restampPsVersion(events) {
122
+ const resolved = this.dims.getPsVersion();
123
+ if (!resolved)
124
+ return events;
125
+ for (const event of events) {
126
+ if (event.type === 'session_summary')
127
+ continue;
128
+ if ('ps_version' in event && event.ps_version === PS_VERSION_UNKNOWN) {
129
+ event.ps_version = resolved;
130
+ }
131
+ }
132
+ return events;
133
+ }
121
134
  enqueue(event) {
122
135
  this.queue.push(event);
123
136
  if (this.queue.length > MAX_QUEUE_SIZE) {
@@ -129,7 +142,7 @@ export class TelemetryClient {
129
142
  async flush() {
130
143
  if (!this.active || this.queue.length === 0)
131
144
  return;
132
- const batch = this.queue.splice(0, this.maxBatchSize).filter(isContentSafe);
145
+ const batch = this.restampPsVersion(this.queue.splice(0, this.maxBatchSize)).filter(isContentSafe);
133
146
  if (batch.length === 0)
134
147
  return;
135
148
  try {
@@ -160,7 +173,7 @@ export class TelemetryClient {
160
173
  }, this.now()));
161
174
  }
162
175
  if (this.queue.length > 0) {
163
- appendOutboxSync(this.queue.splice(0).filter(isContentSafe), this.outboxOpts);
176
+ appendOutboxSync(this.restampPsVersion(this.queue.splice(0)).filter(isContentSafe), this.outboxOpts);
164
177
  }
165
178
  clearSessionState(this.outboxOpts);
166
179
  }
@@ -45,6 +45,7 @@ export function sanitizeMessage(input, maxLen = MAX_MESSAGE_LEN) {
45
45
  }
46
46
  out = redactGenericUserDirs(out);
47
47
  out = basenamePaths(out);
48
+ out = out.replace(/\bnot found: [^\n]*/gi, 'not found: [redacted]');
48
49
  out = out.replace(/\\/g, '/');
49
50
  out = out.replace(/^[/]+/, '');
50
51
  if (out.length > maxLen)
@@ -1,6 +1,7 @@
1
1
  import { runScript } from '../utils/run-script.js';
2
2
  import { validateArgs } from '../utils/validate.js';
3
3
  import { toolErrorResult, runSnippetTool, applyToActiveLayerProp } from '../utils/tool-helpers.js';
4
+ import { getPendingRawDevelop } from '../core/raw-develop-state.js';
4
5
  const addAdjustmentLayerSchema = {
5
6
  type: 'object',
6
7
  properties: {
@@ -669,7 +670,7 @@ export function createAdjustmentTools(connection, snippetClient) {
669
670
  {
670
671
  tool: {
671
672
  name: 'ps_add_adjustment_layer',
672
- description: "Create a non-destructive adjustment layer above the active layer — hoisted out of the active layer's group by default even though Photoshop's own Mk-AdjL placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting). Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for ALL tonal/color adjustments; the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `ps_merge` (mode=visible). Optionally clips the adjustment to only affect the layer directly below it. If a selection is active at call time, the new layer is automatically masked by it (toggle with mask_from_selection / mask_inverted). For destructive ops that don't have an adjustment-layer equivalent in Photoshop (Shadows/Highlights — single-pass shadow/highlight recovery), use `ps_apply_adjustment` (type=shadows_highlights) which auto-duplicates the active layer to keep the original intact. Returns context (the new adjustment layer becomes active) plus parent_path — the actual containing-group chain, so placement is never silent.",
673
+ description: "Create a non-destructive adjustment layer above the active layer — hoisted out of the active layer's group by default even though Photoshop's own Mk-AdjL placement rule would otherwise nest it INSIDE that group (pass into_active_group:true to keep that native nesting). Supports the full real-Photoshop tonal/color surface: Curves (with S-curve presets), Levels, Hue/Saturation, Brightness/Contrast, Black & White (with optional tint), Color Balance, Photo Filter (preset or custom color), Vibrance, Channel Mixer, Selective Color, Gradient Map (preset), Exposure (stops + offset + gamma), Color Lookup (3DLUT presets or custom file path), and Invert. Values are editable, maskable, and removable. This is the canonical entry point for tonal/color adjustments — EXCEPT global tone/color on raw-sourced documents, which starts with a Camera Raw develop pass when a camera-raw develop tool is registered (see ps_open_document's is_raw_source); the old destructive bake tools (auto_levels / auto_contrast / desaturate / invert) were removed on 2026-05-31 — if you genuinely need a pixel bake, follow this call with `ps_merge` (mode=visible). Optionally clips the adjustment to only affect the layer directly below it. If a selection is active at call time, the new layer is automatically masked by it (toggle with mask_from_selection / mask_inverted). For destructive ops that don't have an adjustment-layer equivalent in Photoshop (Shadows/Highlights — single-pass shadow/highlight recovery), use `ps_apply_adjustment` (type=shadows_highlights) which auto-duplicates the active layer to keep the original intact. Returns context (the new adjustment layer becomes active) plus parent_path — the actual containing-group chain, so placement is never silent.",
673
674
  inputSchema: addAdjustmentLayerSchema,
674
675
  outputSchema: {
675
676
  type: 'object',
@@ -695,6 +696,10 @@ export function createAdjustmentTools(connection, snippetClient) {
695
696
  items: { type: 'string' },
696
697
  description: 'The containing-group name chain (outermost first), empty array at the document root.',
697
698
  },
699
+ raw_develop_pending: {
700
+ type: 'string',
701
+ description: 'Advisory, present only when the active document was opened from a raw source this session and no Camera Raw develop pass has run yet. Nothing failed — the layer was created. Global tone/color on raw sources normally starts with the camera-raw develop pass; ignore if the user prescribed this exact layer or the file was already developed elsewhere.',
702
+ },
698
703
  context: { type: 'object' },
699
704
  },
700
705
  },
@@ -833,14 +838,27 @@ async function addAdjustmentLayer(connection, snippetClient, rawArgs) {
833
838
  buildParams.name = name;
834
839
  const script = await snippetClient.build('addAdjustmentLayer', buildParams);
835
840
  const result = await runScript(connection, script);
841
+ const structured = result;
842
+ let advisory = '';
843
+ const pendingRaw = getPendingRawDevelop();
844
+ if (pendingRaw) {
845
+ const source = pendingRaw.documentName || pendingRaw.filePath || 'a raw source';
846
+ const note = `This document was opened from raw source ${source} with no Camera Raw develop pass yet — ` +
847
+ 'consider ps_apply_camera_raw first for global tone/color. Nothing failed; ignore if the ' +
848
+ 'user prescribed this exact layer or the file was already developed elsewhere.';
849
+ if (typeof result === 'object' && result !== null) {
850
+ structured.raw_develop_pending = note;
851
+ }
852
+ advisory = `\nNote: ${note}`;
853
+ }
836
854
  return {
837
855
  content: [
838
856
  {
839
857
  type: 'text',
840
- text: `Adjustment layer added:\n${JSON.stringify(result, null, 2)}`,
858
+ text: `Adjustment layer added:\n${JSON.stringify(result, null, 2)}${advisory}`,
841
859
  },
842
860
  ],
843
- structuredContent: result,
861
+ structuredContent: structured,
844
862
  };
845
863
  }
846
864
  catch (error) {
@@ -174,7 +174,7 @@ export function createDetectionTools(connection, _snippetClient, client = new On
174
174
  {
175
175
  tool: {
176
176
  name: 'ps_detect',
177
- description: 'Locate faces and/or COCO-80 objects (person, dog, car, chair, sofa, …) in the active document using LOCAL on-device computer vision — the image is never sent anywhere. Returns labeled bounding boxes in DOCUMENT-pixel space plus an annotated preview for visual confirmation. Use this to gain semantic scene awareness ("there are 2 people and a dog") and real coordinates before any spatially-targeted edit — far more reliable than estimating positions from a preview. `target` selects faces / objects / both. Read-only: renders a throwaway duplicate, never modifies the working document. Boxes are [x1, y1, x2, y2]. Validate surprising results against the annotated image before acting on them.',
177
+ description: 'The cheap, narrow read: labeled bounding boxes only — faces and/or COCO-80 objects (person, dog, car, chair, sofa, …) in DOCUMENT-pixel space, plus an annotated preview for visual confirmation. LOCAL on-device computer vision; the image is never sent anywhere. Use this for real coordinates before a spatially-targeted edit when boxes are all you need — far more reliable than estimating positions from a preview. For the full scene model (regions, horizon, tonal zones, composition, and a menu of selectable named regions), use ps_read_scene instead. `target` selects faces / objects / both. Read-only: renders a throwaway duplicate, never modifies the working document. Boxes are [x1, y1, x2, y2]. Validate surprising results against the annotated image before acting on them.',
178
178
  inputSchema: detectSchema,
179
179
  outputSchema: {
180
180
  type: 'object',