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.
- package/README.md +7 -7
- package/dist/api/extendscript/_helpers.js +64 -2
- package/dist/api/photoshop-api.js +1 -1
- package/dist/bin/editmamei-core-darwin-arm64 +0 -0
- package/dist/bin/editmamei-core-darwin-x64 +0 -0
- package/dist/bin/editmamei-core-win-x64.exe +0 -0
- package/dist/cli/activate.js +1 -1
- package/dist/cli/config.js +1 -1
- package/dist/cli/deactivate.js +1 -1
- package/dist/core/raw-develop-state.js +13 -0
- package/dist/core/server.js +64 -3
- package/dist/core/tool-groups.js +14 -12
- package/dist/core/tool-tiers.js +4 -2
- package/dist/detection/runtime.js +2 -2
- package/dist/perception/region-scorer.js +1 -1
- package/dist/platform/connection.js +15 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/client.js +15 -2
- package/dist/telemetry/sanitize.js +1 -0
- package/dist/tools/adjustment-tools.js +21 -3
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/document-tools.js +5 -2
- package/dist/tools/filter-tools.js +130 -13
- package/dist/tools/group-tools.js +172 -54
- package/dist/tools/history-tools.js +1 -1
- package/dist/tools/inspect-tools.js +22 -3
- package/dist/tools/layer-tools.js +4 -4
- package/dist/tools/overview-tools.js +37 -8
- package/dist/tools/preview-tools.js +3 -2
- package/dist/tools/scene-tools.js +1 -1
- package/dist/tools/selection-tools.js +19 -17
- package/dist/tools/smart-object-tools.js +163 -0
- package/dist/tools/text-tools.js +92 -2
- package/dist/utils/session-log.js +42 -6
- package/dist/utils/temp.js +1 -1
- package/dist/utils/tool-helpers.js +23 -0
- package/dist/version.js +1 -1
- 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
|
|
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
|
-
**
|
|
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 | `
|
|
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` · `
|
|
91
|
-
| **Masks & paths** | Community | `layer_mask` · `
|
|
92
|
-
| **Type** | Community | `
|
|
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
|
|
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.
|
|
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
|
|
Binary file
|
package/dist/cli/activate.js
CHANGED
package/dist/cli/config.js
CHANGED
|
@@ -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);
|
package/dist/cli/deactivate.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/server.js
CHANGED
|
@@ -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:
|
|
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: [
|
package/dist/core/tool-groups.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
70
|
-
purpose: 'Face-landmark perception and mesh-aimed feature selection / contour strokes
|
|
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
|
-
|
|
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
|
-
|
|
154
|
+
ps_text: 'type',
|
|
153
155
|
ps_create_text_layer: 'type',
|
|
154
156
|
ps_set_text: 'type',
|
|
155
157
|
ps_detect: 'perception',
|
package/dist/core/tool-tiers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -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
|
package/dist/telemetry/client.js
CHANGED
|
@@ -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
|
|
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:
|
|
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: '
|
|
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',
|