editmamei 1.3.0 → 1.4.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 +6 -5
- 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/core/server.js +3 -3
- package/dist/core/tool-groups.js +3 -3
- package/dist/core/tool-registry.js +3 -1
- package/dist/core/tool-tiers.js +1 -1
- package/dist/kernel/kernel.js +1 -0
- package/dist/modules/ce/index.js +7 -0
- package/dist/perception/facets.js +4 -3
- package/dist/perception/region-scorer.js +1 -1
- package/dist/perception/scene-model.js +1 -1
- package/dist/perception/select-recipes.js +14 -3
- package/dist/platform/launch-readiness.js +25 -0
- package/dist/platform/macos-runner.js +18 -8
- package/dist/platform/script-queue.js +5 -19
- package/dist/platform/windows-runner.js +18 -8
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/telemetry/client.js +11 -4
- package/dist/telemetry/events.js +5 -2
- package/dist/tools/detection-tools.js +4 -4
- package/dist/tools/preview-tools.js +1 -1
- package/dist/tools/scene-tools.js +8 -6
- package/dist/tools/selection-tools.js +155 -109
- package/dist/tools/sequence-tools.js +435 -0
- package/dist/utils/operation-timeouts.js +92 -0
- package/dist/utils/run-script.js +25 -1
- package/dist/utils/session-log.js +15 -1
- package/dist/utils/tool-budget-context.js +14 -0
- package/dist/version.js +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { validateArgs } from '../utils/validate.js';
|
|
2
|
+
import { toolErrorResult } from '../utils/tool-helpers.js';
|
|
3
|
+
import { SEQUENCE_OVERALL_TIMEOUT_MS } from '../utils/operation-timeouts.js';
|
|
4
|
+
const SEQUENCE_TOOL_NAME = 'ps_sequence';
|
|
5
|
+
const MIN_STEPS = 1;
|
|
6
|
+
const MAX_STEPS = 25;
|
|
7
|
+
export const HISTORY_UNSAFE_TOOLS = new Set([
|
|
8
|
+
'ps_open_document',
|
|
9
|
+
'ps_create_document',
|
|
10
|
+
'ps_close_document',
|
|
11
|
+
'ps_save_psd',
|
|
12
|
+
'ps_export',
|
|
13
|
+
'ps_document',
|
|
14
|
+
'ps_undo',
|
|
15
|
+
'ps_redo',
|
|
16
|
+
]);
|
|
17
|
+
const HISTORY_UNSAFE_TOOLS_LIST = Array.from(HISTORY_UNSAFE_TOOLS).join(', ');
|
|
18
|
+
const stepItemSchema = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
description: 'One call: `tool` is a registered tool name, `args` is the object it would normally receive.',
|
|
21
|
+
properties: {
|
|
22
|
+
tool: { type: 'string', description: 'A tool name from tools/list, e.g. "ps_set_layer".' },
|
|
23
|
+
args: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
description: 'Arguments for that call, exactly as the named tool expects them.',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ['tool'],
|
|
29
|
+
};
|
|
30
|
+
const sequenceSchema = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
steps: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
description: `Ordered list of tool calls to run against the current document, ${MIN_STEPS} to ${MAX_STEPS} items.`,
|
|
36
|
+
items: stepItemSchema,
|
|
37
|
+
minItems: MIN_STEPS,
|
|
38
|
+
maxItems: MAX_STEPS,
|
|
39
|
+
},
|
|
40
|
+
on_error: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
enum: ['stop', 'continue', 'rollback'],
|
|
43
|
+
default: 'stop',
|
|
44
|
+
description: `stop: halt at the first failing step and return results so far. continue: record the failure and run every remaining step anyway. rollback: on the first failing step, undo back to the history state captured before step one and VERIFY the document actually landed there (index, state name, and active document all re-checked) before reporting success — refused at VALIDATION time (before any step runs) if any step names one of: ${HISTORY_UNSAFE_TOOLS_LIST}, since those sit outside what the history cursor can undo. Assumes nothing else edits the document while the sequence runs; a manual edit interleaved between steps corrupts the undo distance rollback computes.`,
|
|
45
|
+
},
|
|
46
|
+
return: {
|
|
47
|
+
type: 'string',
|
|
48
|
+
enum: ['summary', 'full'],
|
|
49
|
+
default: 'summary',
|
|
50
|
+
description: "summary: one line per step plus the LAST step's full result. full: every step's full result — note this means every embedded payload (not just the last step's) lands in the logged call record. Either way, an inline preview (image content) is stripped from every step except the last — previews are most of a result's bytes, and a sequence exists to stop paying for them on intermediate steps.",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ['steps'],
|
|
54
|
+
};
|
|
55
|
+
const stepResultItemSchema = {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
index: { type: 'number' },
|
|
59
|
+
tool: { type: 'string' },
|
|
60
|
+
ok: { type: 'boolean' },
|
|
61
|
+
duration_ms: { type: 'number' },
|
|
62
|
+
text: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: "The first line of that step's own result text (return='summary').",
|
|
65
|
+
},
|
|
66
|
+
result: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
description: "That step's full CallToolResult (return='full').",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ['index', 'tool', 'ok'],
|
|
72
|
+
};
|
|
73
|
+
const sequenceOutputSchema = {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
on_error: { type: 'string' },
|
|
77
|
+
return: { type: 'string' },
|
|
78
|
+
total_steps: { type: 'number' },
|
|
79
|
+
ran_steps: {
|
|
80
|
+
type: 'number',
|
|
81
|
+
description: 'How many steps actually ran — excludes a step skipped by the time budget.',
|
|
82
|
+
},
|
|
83
|
+
failed_step: {
|
|
84
|
+
type: ['object', 'null'],
|
|
85
|
+
description: 'The first failing step, or the step skipped by the overall time budget. Null when every step succeeded.',
|
|
86
|
+
properties: {
|
|
87
|
+
index: { type: 'number' },
|
|
88
|
+
tool: { type: 'string' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
cap_exceeded: { type: 'boolean' },
|
|
92
|
+
rolled_back: { type: 'boolean' },
|
|
93
|
+
rollback_reason: {
|
|
94
|
+
type: 'string',
|
|
95
|
+
description: 'Set only when on_error="rollback" and rolled_back is false: history_evicted, undo_failed, cursor_moved_backward, or document_changed.',
|
|
96
|
+
enum: ['history_evicted', 'undo_failed', 'cursor_moved_backward', 'document_changed'],
|
|
97
|
+
},
|
|
98
|
+
steps: {
|
|
99
|
+
type: 'array',
|
|
100
|
+
description: 'One entry per step run — carries `text` in summary mode, `result` in full mode.',
|
|
101
|
+
items: stepResultItemSchema,
|
|
102
|
+
},
|
|
103
|
+
final: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
description: "The last step's full CallToolResult (summary mode only).",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ['on_error', 'return', 'total_steps', 'ran_steps', 'steps'],
|
|
109
|
+
};
|
|
110
|
+
function validateSequenceArgs(rawArgs, hasTool) {
|
|
111
|
+
const args = validateArgs(sequenceSchema, rawArgs);
|
|
112
|
+
const rawSteps = args.steps;
|
|
113
|
+
if (!Array.isArray(rawSteps)) {
|
|
114
|
+
throw new Error('"steps" must be an array');
|
|
115
|
+
}
|
|
116
|
+
if (rawSteps.length < MIN_STEPS || rawSteps.length > MAX_STEPS) {
|
|
117
|
+
throw new Error(`"steps" must contain ${MIN_STEPS} to ${MAX_STEPS} items, got ${rawSteps.length}`);
|
|
118
|
+
}
|
|
119
|
+
const steps = rawSteps.map((raw, i) => {
|
|
120
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
121
|
+
throw new Error(`step ${i + 1}: expected an object with "tool" and "args"`);
|
|
122
|
+
}
|
|
123
|
+
const stepObj = raw;
|
|
124
|
+
const tool = stepObj.tool;
|
|
125
|
+
if (typeof tool !== 'string' || tool.length === 0) {
|
|
126
|
+
throw new Error(`step ${i + 1}: "tool" must be a non-empty string`);
|
|
127
|
+
}
|
|
128
|
+
if (tool === SEQUENCE_TOOL_NAME) {
|
|
129
|
+
throw new Error(`step ${i + 1}: cannot nest ${SEQUENCE_TOOL_NAME} inside itself`);
|
|
130
|
+
}
|
|
131
|
+
if (!hasTool(tool)) {
|
|
132
|
+
throw new Error(`step ${i + 1}: "${tool}" is not a tool registered right now`);
|
|
133
|
+
}
|
|
134
|
+
const rawStepArgs = stepObj.args;
|
|
135
|
+
if (rawStepArgs !== undefined &&
|
|
136
|
+
(typeof rawStepArgs !== 'object' || rawStepArgs === null || Array.isArray(rawStepArgs))) {
|
|
137
|
+
throw new Error(`step ${i + 1}: "args" must be an object`);
|
|
138
|
+
}
|
|
139
|
+
return { tool, args: rawStepArgs ?? {} };
|
|
140
|
+
});
|
|
141
|
+
const onError = args.on_error;
|
|
142
|
+
const returnMode = args.return;
|
|
143
|
+
if (onError === 'rollback') {
|
|
144
|
+
const firstUnsafe = steps.findIndex((s) => HISTORY_UNSAFE_TOOLS.has(s.tool));
|
|
145
|
+
if (firstUnsafe !== -1) {
|
|
146
|
+
const tool = steps[firstUnsafe].tool;
|
|
147
|
+
throw new Error(`step ${firstUnsafe + 1}: on_error="rollback" can't be honored because "${tool}" is one of the tools ps_sequence treats as unsafe for rollback (${HISTORY_UNSAFE_TOOLS_LIST}) — outside what the history cursor can undo. Use on_error="stop" or "continue", or drop that step.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { steps, onError, returnMode };
|
|
151
|
+
}
|
|
152
|
+
function syntheticError(message) {
|
|
153
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
154
|
+
}
|
|
155
|
+
function firstTextLine(result) {
|
|
156
|
+
const block = result.content?.find((b) => b.type === 'text');
|
|
157
|
+
if (!block || !('text' in block))
|
|
158
|
+
return '';
|
|
159
|
+
const newline = block.text.indexOf('\n');
|
|
160
|
+
return newline === -1 ? block.text : block.text.slice(0, newline);
|
|
161
|
+
}
|
|
162
|
+
function stripImages(result) {
|
|
163
|
+
if (!Array.isArray(result.content))
|
|
164
|
+
return result;
|
|
165
|
+
const kept = result.content.filter((b) => b.type !== 'image');
|
|
166
|
+
return kept.length === result.content.length ? result : { ...result, content: kept };
|
|
167
|
+
}
|
|
168
|
+
function extractHistorySnapshot(result) {
|
|
169
|
+
if (result.isError)
|
|
170
|
+
return null;
|
|
171
|
+
const sc = result.structuredContent;
|
|
172
|
+
const index = sc?.currentIndex;
|
|
173
|
+
const stateName = sc?.currentState;
|
|
174
|
+
const totalStates = sc?.totalStates;
|
|
175
|
+
const documentName = sc?.context?.document?.name;
|
|
176
|
+
if (typeof index !== 'number' ||
|
|
177
|
+
!Number.isFinite(index) ||
|
|
178
|
+
typeof stateName !== 'string' ||
|
|
179
|
+
typeof totalStates !== 'number' ||
|
|
180
|
+
!Number.isFinite(totalStates)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
index,
|
|
185
|
+
stateName,
|
|
186
|
+
totalStates,
|
|
187
|
+
documentName: typeof documentName === 'string' ? documentName : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
async function readHistorySnapshot(invokeTool) {
|
|
191
|
+
const result = await invokeTool('ps_inspect', { what: 'history' });
|
|
192
|
+
return extractHistorySnapshot(result);
|
|
193
|
+
}
|
|
194
|
+
async function performRollback(invokeTool, captured) {
|
|
195
|
+
try {
|
|
196
|
+
const before = await readHistorySnapshot(invokeTool);
|
|
197
|
+
if (before === null) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
reason: 'undo_failed',
|
|
201
|
+
message: 'could not read the document history to compute the rollback distance',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (before.documentName !== captured.documentName) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
reason: 'document_changed',
|
|
208
|
+
message: `the active document changed since step 1 (captured "${captured.documentName}", now "${before.documentName}") — history is per-document, so nothing was undone`,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const delta = before.index - captured.index;
|
|
212
|
+
if (delta < 0) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
reason: 'cursor_moved_backward',
|
|
216
|
+
message: `the history cursor is already earlier than the captured state (captured index ${captured.index}, now ${before.index}) — a step likely called undo or redo itself`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (delta > 0) {
|
|
220
|
+
const undone = await invokeTool('ps_undo', { steps: delta });
|
|
221
|
+
if (undone.isError) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
reason: 'undo_failed',
|
|
225
|
+
message: `ps_undo failed: ${firstTextLine(undone)}`,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const after = await readHistorySnapshot(invokeTool);
|
|
230
|
+
if (after === null) {
|
|
231
|
+
return {
|
|
232
|
+
ok: false,
|
|
233
|
+
reason: 'undo_failed',
|
|
234
|
+
message: 'could not re-read the document history to verify the rollback',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (after.documentName !== captured.documentName) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: 'document_changed',
|
|
241
|
+
message: `the active document changed during rollback (captured "${captured.documentName}", now "${after.documentName}")`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
if (after.index !== captured.index || after.stateName !== captured.stateName) {
|
|
245
|
+
return {
|
|
246
|
+
ok: false,
|
|
247
|
+
reason: 'history_evicted',
|
|
248
|
+
message: `the history state Photoshop reports at index ${after.index} ("${after.stateName}") does not match what was captured ` +
|
|
249
|
+
`(index ${captured.index}, "${captured.stateName}") — Photoshop's history buffer is finite and likely evicted it ` +
|
|
250
|
+
`(captured ${captured.totalStates} total states, now ${after.totalStates})`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
ok: true,
|
|
255
|
+
message: delta > 0
|
|
256
|
+
? `document verified restored to its state before step 1 ("${captured.stateName}", ${delta} step${delta === 1 ? '' : 's'} undone)`
|
|
257
|
+
: `document verified already at its state before step 1 ("${captured.stateName}"); no edits to undo`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
262
|
+
return { ok: false, reason: 'undo_failed', message: `rollback failed: ${msg}` };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function buildMessage(input) {
|
|
266
|
+
const { ranSteps, total, onError, failedStep, capExceeded, rolledBack, rollbackMessage } = input;
|
|
267
|
+
if (capExceeded && failedStep) {
|
|
268
|
+
const base = `Sequence exceeded its overall time budget (${SEQUENCE_OVERALL_TIMEOUT_MS}ms) before step ${failedStep.index + 1} (${failedStep.tool}) could run; stopped after ${ranSteps}/${total} step(s).`;
|
|
269
|
+
if (onError !== 'rollback')
|
|
270
|
+
return base;
|
|
271
|
+
return rolledBack
|
|
272
|
+
? `${base} Document rolled back: ${rollbackMessage}.`
|
|
273
|
+
: `${base} Rollback did not complete: ${rollbackMessage}.`;
|
|
274
|
+
}
|
|
275
|
+
if (!failedStep) {
|
|
276
|
+
return `Sequence completed all ${total} step(s).`;
|
|
277
|
+
}
|
|
278
|
+
if (onError === 'continue') {
|
|
279
|
+
return `Sequence completed all ${total} step(s); step ${failedStep.index + 1} (${failedStep.tool}) failed and was recorded.`;
|
|
280
|
+
}
|
|
281
|
+
if (onError === 'rollback') {
|
|
282
|
+
return rolledBack
|
|
283
|
+
? `Sequence stopped at step ${failedStep.index + 1} (${failedStep.tool}) after a failure; ${rollbackMessage}.`
|
|
284
|
+
: `Sequence stopped at step ${failedStep.index + 1} (${failedStep.tool}) after a failure; rollback did not complete: ${rollbackMessage}.`;
|
|
285
|
+
}
|
|
286
|
+
return `Sequence stopped at step ${failedStep.index + 1} (${failedStep.tool}) after a failure; ${ranSteps}/${total} step(s) ran.`;
|
|
287
|
+
}
|
|
288
|
+
async function runSequence(invokeTool, hasTool, rawArgs, now) {
|
|
289
|
+
let parsed;
|
|
290
|
+
try {
|
|
291
|
+
parsed = validateSequenceArgs(rawArgs, hasTool);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
return toolErrorResult('Error validating sequence', error);
|
|
295
|
+
}
|
|
296
|
+
const { steps, onError, returnMode } = parsed;
|
|
297
|
+
let capturedHistory = null;
|
|
298
|
+
if (onError === 'rollback') {
|
|
299
|
+
let captured;
|
|
300
|
+
try {
|
|
301
|
+
captured = await readHistorySnapshot(invokeTool);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return toolErrorResult('Error capturing history state before the sequence', error);
|
|
305
|
+
}
|
|
306
|
+
if (captured === null) {
|
|
307
|
+
return toolErrorResult('Error capturing history state before the sequence', new Error('ps_inspect(what="history") did not return a usable history snapshot'));
|
|
308
|
+
}
|
|
309
|
+
capturedHistory = captured;
|
|
310
|
+
}
|
|
311
|
+
const startedAt = now();
|
|
312
|
+
const entries = [];
|
|
313
|
+
let failedStep = null;
|
|
314
|
+
let capExceeded = false;
|
|
315
|
+
let rolledBack = false;
|
|
316
|
+
let rollbackReason;
|
|
317
|
+
let rollbackMessage = '';
|
|
318
|
+
for (let i = 0; i < steps.length; i++) {
|
|
319
|
+
const step = steps[i];
|
|
320
|
+
if (now() - startedAt > SEQUENCE_OVERALL_TIMEOUT_MS) {
|
|
321
|
+
capExceeded = true;
|
|
322
|
+
if (failedStep === null)
|
|
323
|
+
failedStep = { index: i, tool: step.tool };
|
|
324
|
+
entries.push({
|
|
325
|
+
index: i,
|
|
326
|
+
tool: step.tool,
|
|
327
|
+
ok: false,
|
|
328
|
+
duration_ms: 0,
|
|
329
|
+
result: syntheticError(`Sequence exceeded its overall time budget (${SEQUENCE_OVERALL_TIMEOUT_MS}ms) before this step could run.`),
|
|
330
|
+
});
|
|
331
|
+
if (onError === 'rollback') {
|
|
332
|
+
const r = await performRollback(invokeTool, capturedHistory);
|
|
333
|
+
rolledBack = r.ok;
|
|
334
|
+
rollbackReason = r.reason;
|
|
335
|
+
rollbackMessage = r.message;
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
const stepStarted = now();
|
|
340
|
+
let result;
|
|
341
|
+
try {
|
|
342
|
+
result = await invokeTool(step.tool, step.args);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
result = syntheticError(error instanceof Error ? error.message : String(error));
|
|
346
|
+
}
|
|
347
|
+
const duration_ms = now() - stepStarted;
|
|
348
|
+
const ok = !result.isError;
|
|
349
|
+
entries.push({ index: i, tool: step.tool, ok, duration_ms, result });
|
|
350
|
+
if (!ok) {
|
|
351
|
+
if (failedStep === null)
|
|
352
|
+
failedStep = { index: i, tool: step.tool };
|
|
353
|
+
if (onError === 'rollback') {
|
|
354
|
+
const r = await performRollback(invokeTool, capturedHistory);
|
|
355
|
+
rolledBack = r.ok;
|
|
356
|
+
rollbackReason = r.reason;
|
|
357
|
+
rollbackMessage = r.message;
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (onError === 'stop') {
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const lastRealIndex = capExceeded ? entries.length - 2 : entries.length - 1;
|
|
366
|
+
const stripped = entries.map((e, idx) => idx === lastRealIndex ? e : { ...e, result: stripImages(e.result) });
|
|
367
|
+
const ranSteps = capExceeded ? entries.length - 1 : entries.length;
|
|
368
|
+
const anyFailure = failedStep !== null;
|
|
369
|
+
const isError = capExceeded || (anyFailure && onError !== 'continue');
|
|
370
|
+
const message = buildMessage({
|
|
371
|
+
ranSteps,
|
|
372
|
+
total: steps.length,
|
|
373
|
+
onError,
|
|
374
|
+
failedStep,
|
|
375
|
+
capExceeded,
|
|
376
|
+
rolledBack,
|
|
377
|
+
rollbackMessage,
|
|
378
|
+
});
|
|
379
|
+
const common = {
|
|
380
|
+
on_error: onError,
|
|
381
|
+
return: returnMode,
|
|
382
|
+
total_steps: steps.length,
|
|
383
|
+
ran_steps: ranSteps,
|
|
384
|
+
failed_step: failedStep,
|
|
385
|
+
cap_exceeded: capExceeded,
|
|
386
|
+
rolled_back: rolledBack,
|
|
387
|
+
...(rollbackReason ? { rollback_reason: rollbackReason } : {}),
|
|
388
|
+
};
|
|
389
|
+
const structuredContent = returnMode === 'full'
|
|
390
|
+
? {
|
|
391
|
+
...common,
|
|
392
|
+
steps: stripped.map((e) => ({
|
|
393
|
+
index: e.index,
|
|
394
|
+
tool: e.tool,
|
|
395
|
+
ok: e.ok,
|
|
396
|
+
duration_ms: e.duration_ms,
|
|
397
|
+
result: e.result,
|
|
398
|
+
})),
|
|
399
|
+
}
|
|
400
|
+
: {
|
|
401
|
+
...common,
|
|
402
|
+
steps: stripped.map((e) => ({
|
|
403
|
+
index: e.index,
|
|
404
|
+
tool: e.tool,
|
|
405
|
+
ok: e.ok,
|
|
406
|
+
duration_ms: e.duration_ms,
|
|
407
|
+
text: firstTextLine(e.result),
|
|
408
|
+
})),
|
|
409
|
+
final: lastRealIndex >= 0 ? stripped[lastRealIndex].result : undefined,
|
|
410
|
+
};
|
|
411
|
+
return {
|
|
412
|
+
content: [{ type: 'text', text: message }],
|
|
413
|
+
structuredContent,
|
|
414
|
+
...(isError ? { isError: true } : {}),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
export function createSequenceTools(invokeTool, hasTool, opts = {}) {
|
|
418
|
+
const { now = Date.now } = opts;
|
|
419
|
+
return [
|
|
420
|
+
{
|
|
421
|
+
tool: {
|
|
422
|
+
name: SEQUENCE_TOOL_NAME,
|
|
423
|
+
description: "Run an ordered list of tool calls against the current document in ONE round trip. WHEN TO REACH FOR THIS: several dependent steps you already know you want (e.g. select → adjust → merge, or a repeated resize/export pass) where you do not need to look at the result between them — each step is dispatched the same way an ordinary call is and sees the document exactly as the previous step left it. Not for exploratory work: if the next step depends on inspecting this one first, call the tools individually instead. Every step must name a tool that already exists in this edition (ps_sequence cannot call itself). An inline preview (image content) returned by a step is dropped unless that step is the LAST one in the sequence, since previews are most of a result's bytes and the point of batching calls is to stop paying for them on every intermediate step. Each step keeps its own time limit, exactly as it would if you called it on its own. The sequence's overall budget only decides whether to START another step, so it never cuts one off mid-run — a call can therefore finish after that ceiling by however long the in-flight step still needs, plus the undo and its verification reads when on_error='rollback'. With return='full', every step's complete result (not just the last one's) lands in the logged call payload.",
|
|
424
|
+
inputSchema: sequenceSchema,
|
|
425
|
+
outputSchema: sequenceOutputSchema,
|
|
426
|
+
annotations: {
|
|
427
|
+
title: 'Run a Tool Sequence',
|
|
428
|
+
destructiveHint: true,
|
|
429
|
+
idempotentHint: false,
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
handler: async (args) => runSequence(invokeTool, hasTool, args, now),
|
|
433
|
+
},
|
|
434
|
+
];
|
|
435
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { Logger } from './logger.js';
|
|
2
|
+
const logger = new Logger('operation-timeouts');
|
|
3
|
+
export const DEFAULT_SCRIPT_TIMEOUT_MS = 30_000;
|
|
4
|
+
export const SCRIPT_TIMEOUT_FLOOR_MS = 5_000;
|
|
1
5
|
export const OPEN_DOCUMENT_TIMEOUT_MS = 120_000;
|
|
2
6
|
export const OPEN_DOCUMENT_REPROBE_TIMEOUT_MS = 10_000;
|
|
3
7
|
export const CAMERA_RAW_FILTER_TIMEOUT_MS = 120_000;
|
|
@@ -7,3 +11,91 @@ export const SELECT_FOCUS_AREA_TIMEOUT_MS = 120_000;
|
|
|
7
11
|
export const SKY_REPLACEMENT_TIMEOUT_MS = 120_000;
|
|
8
12
|
export const ANNOTATED_PREVIEW_TIMEOUT_MS = 90_000;
|
|
9
13
|
export const SCENE_CHANNEL_TIMEOUT_MS = 120_000;
|
|
14
|
+
export const SEQUENCE_OVERALL_TIMEOUT_MS = 300_000;
|
|
15
|
+
export const TOOL_TIMEOUT_BUDGETS_MS = {
|
|
16
|
+
ps_open_document: OPEN_DOCUMENT_TIMEOUT_MS,
|
|
17
|
+
ps_select_subject: SELECT_SUBJECT_TIMEOUT_MS,
|
|
18
|
+
ps_select_sky: SELECT_SKY_TIMEOUT_MS,
|
|
19
|
+
ps_replace_sky: SKY_REPLACEMENT_TIMEOUT_MS,
|
|
20
|
+
ps_ping: 30_000,
|
|
21
|
+
ps_read_scene: 164_000,
|
|
22
|
+
ps_selection_channel: 123_000,
|
|
23
|
+
ps_select: 54_000,
|
|
24
|
+
ps_modify_selection: 37_000,
|
|
25
|
+
ps_save_psd: 32_000,
|
|
26
|
+
ps_filter: 31_000,
|
|
27
|
+
ps_get_histogram: 25_000,
|
|
28
|
+
ps_create_document: 20_000,
|
|
29
|
+
ps_detect: 19_000,
|
|
30
|
+
ps_get_preview: 17_000,
|
|
31
|
+
ps_resize_image: 16_000,
|
|
32
|
+
ps_get_selection_preview: 14_000,
|
|
33
|
+
ps_undo: 12_000,
|
|
34
|
+
ps_export: 9_000,
|
|
35
|
+
ps_path: 10_000,
|
|
36
|
+
ps_crop_document: 10_000,
|
|
37
|
+
ps_inspect: 7_000,
|
|
38
|
+
ps_duplicate_layer: 7_000,
|
|
39
|
+
ps_shape: 7_000,
|
|
40
|
+
ps_text: 7_000,
|
|
41
|
+
ps_set_layer: 6_000,
|
|
42
|
+
ps_bake_layer: 6_000,
|
|
43
|
+
ps_document: 30_000,
|
|
44
|
+
ps_apply_adjustment: 30_000,
|
|
45
|
+
ps_close_document: 30_000,
|
|
46
|
+
ps_group: 30_000,
|
|
47
|
+
ps_clipping_mask: 30_000,
|
|
48
|
+
ps_convert_image_mode: 30_000,
|
|
49
|
+
ps_convert_to_smart_object: 30_000,
|
|
50
|
+
ps_create_layer: 30_000,
|
|
51
|
+
ps_fill_layer: 30_000,
|
|
52
|
+
ps_select_layer: 30_000,
|
|
53
|
+
ps_transform_layer: 30_000,
|
|
54
|
+
ps_transform_canvas: 30_000,
|
|
55
|
+
ps_guides: 30_000,
|
|
56
|
+
ps_retouch: 30_000,
|
|
57
|
+
ps_select_by_reference: 30_000,
|
|
58
|
+
ps_list_capabilities: 5_000,
|
|
59
|
+
ps_add_adjustment_layer: 5_000,
|
|
60
|
+
ps_redo: 5_000,
|
|
61
|
+
ps_place_image: 5_000,
|
|
62
|
+
ps_move_layer_to_position: 5_000,
|
|
63
|
+
ps_rasterize_layer: 5_000,
|
|
64
|
+
ps_copy_to_new_layer: 5_000,
|
|
65
|
+
ps_merge: 5_000,
|
|
66
|
+
ps_add_layer_style: 5_000,
|
|
67
|
+
ps_delete_layer: 5_000,
|
|
68
|
+
ps_add_fill_layer: 5_000,
|
|
69
|
+
ps_overview: 5_000,
|
|
70
|
+
ps_get_layer_bounds_diff: 5_000,
|
|
71
|
+
ps_compare_regions: 5_000,
|
|
72
|
+
ps_layer_mask: 5_000,
|
|
73
|
+
ps_vector_mask: 5_000,
|
|
74
|
+
ps_apply_image: 5_000,
|
|
75
|
+
ps_sequence: Number.POSITIVE_INFINITY,
|
|
76
|
+
};
|
|
77
|
+
export const TOOLS_WITHOUT_A_BUDGET = {
|
|
78
|
+
ps_report_problem: 'n=3 measured calls',
|
|
79
|
+
ps_calculations: 'n=8 measured calls',
|
|
80
|
+
};
|
|
81
|
+
const MAX_SCRIPT_TIMEOUT_SCALE = 10;
|
|
82
|
+
function resolveScriptTimeoutScale() {
|
|
83
|
+
const raw = process.env.EDITMAMEI_SCRIPT_TIMEOUT_MS;
|
|
84
|
+
if (!raw)
|
|
85
|
+
return 1;
|
|
86
|
+
const n = Number(raw);
|
|
87
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
88
|
+
logger.warn(`EDITMAMEI_SCRIPT_TIMEOUT_MS="${raw}" is not a positive number — ignoring it.`);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
return Math.min(n / DEFAULT_SCRIPT_TIMEOUT_MS, MAX_SCRIPT_TIMEOUT_SCALE);
|
|
92
|
+
}
|
|
93
|
+
export const SCRIPT_TIMEOUT_SCALE = resolveScriptTimeoutScale();
|
|
94
|
+
export function getToolTimeoutMs(toolName) {
|
|
95
|
+
const base = Object.hasOwn(TOOL_TIMEOUT_BUDGETS_MS, toolName)
|
|
96
|
+
? TOOL_TIMEOUT_BUDGETS_MS[toolName]
|
|
97
|
+
: DEFAULT_SCRIPT_TIMEOUT_MS;
|
|
98
|
+
if (!Number.isFinite(base))
|
|
99
|
+
return base;
|
|
100
|
+
return Math.max(Math.round(base * SCRIPT_TIMEOUT_SCALE), SCRIPT_TIMEOUT_FLOOR_MS);
|
|
101
|
+
}
|
package/dist/utils/run-script.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PhotoshopAPIFactory } from '../api/photoshop-api.js';
|
|
2
|
+
import { currentToolBudget } from './tool-budget-context.js';
|
|
2
3
|
const apiCache = new WeakMap();
|
|
3
4
|
export async function runScript(connection, script, timeoutMs) {
|
|
4
5
|
let api = apiCache.get(connection);
|
|
@@ -7,5 +8,28 @@ export async function runScript(connection, script, timeoutMs) {
|
|
|
7
8
|
api = await apiFactory.createAPI();
|
|
8
9
|
apiCache.set(connection, api);
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
const budget = currentToolBudget();
|
|
12
|
+
let effectiveTimeoutMs = timeoutMs;
|
|
13
|
+
if (timeoutMs === undefined && budget) {
|
|
14
|
+
const remaining = budget.deadline - Date.now();
|
|
15
|
+
if (remaining <= 0) {
|
|
16
|
+
throw new Error(`Tool '${budget.toolName}' exceeded its ${budget.budgetMs}ms budget before this script could run.`);
|
|
17
|
+
}
|
|
18
|
+
effectiveTimeoutMs = remaining;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
return await api.executeScript(script, effectiveTimeoutMs);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (budget && isScriptTimeoutError(error)) {
|
|
25
|
+
throw new Error(`Tool '${budget.toolName}' exceeded its ${effectiveTimeoutMs}ms budget: ${error.message}`, { cause: error });
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isScriptTimeoutError(error) {
|
|
31
|
+
if (!(error instanceof Error))
|
|
32
|
+
return false;
|
|
33
|
+
return (error.message.startsWith('Script execution timeout') ||
|
|
34
|
+
/AppleEvent timed out|-1712\b/.test(error.message));
|
|
11
35
|
}
|
|
@@ -51,6 +51,10 @@ export const ERROR_CLASS_TABLE = [
|
|
|
51
51
|
errorClass: 'file_io',
|
|
52
52
|
pattern: /\b(ENOENT|EBUSY|EACCES|EPERM|EMFILE|ENOSPC)\b/,
|
|
53
53
|
},
|
|
54
|
+
{
|
|
55
|
+
errorClass: 'unknown_discriminator',
|
|
56
|
+
pattern: /unknown [^"]{1,40}"[^"]*"\. Allowed: /i,
|
|
57
|
+
},
|
|
54
58
|
{
|
|
55
59
|
errorClass: 'schema_validation',
|
|
56
60
|
pattern: /\bvalidat|required.*field|missing required argument|must be.*type|invalid (input|argument)/i,
|
|
@@ -76,6 +80,7 @@ export const ERROR_CLASS_TABLE = [
|
|
|
76
80
|
errorClass: 'wrong_layer_kind',
|
|
77
81
|
pattern: /pixel layer|text layer|smart object layer|layer kind|rasterize it first/i,
|
|
78
82
|
},
|
|
83
|
+
{ errorClass: 'channel_not_visible', pattern: /histogram for visible channels/i },
|
|
79
84
|
{ errorClass: 'ps_command_unavailable', pattern: /not currently available/i },
|
|
80
85
|
{ errorClass: 'timeout', pattern: /timed? ?out|Script execution timeout|exceeded.*bytes/i },
|
|
81
86
|
{
|
|
@@ -85,6 +90,10 @@ export const ERROR_CLASS_TABLE = [
|
|
|
85
90
|
{ errorClass: 'ai_selection_no_result', pattern: /returned no result/i },
|
|
86
91
|
{ errorClass: 'ps_general_error', pattern: /general photoshop error/i },
|
|
87
92
|
{ errorClass: 'ps_no_such_element', pattern: /no such element/i },
|
|
93
|
+
{
|
|
94
|
+
errorClass: 'extendscript_reference_error',
|
|
95
|
+
pattern: /\b[A-Za-z_$][\w$]*\s+is undefined\s*$/,
|
|
96
|
+
},
|
|
88
97
|
{ errorClass: 'write_not_verified', pattern: /did not verify/i },
|
|
89
98
|
{ errorClass: 'ps_empty_error', pattern: /:\s*$/ },
|
|
90
99
|
{ errorClass: 'ps_op_failed', pattern: /failed[: (]/i },
|
|
@@ -98,6 +107,11 @@ export function classifyError(error) {
|
|
|
98
107
|
}
|
|
99
108
|
return 'other';
|
|
100
109
|
}
|
|
110
|
+
export const NO_ERROR_TEXT_CLASS = 'no_error_text';
|
|
111
|
+
export const ALL_ERROR_CLASSES = [
|
|
112
|
+
...ERROR_CLASS_TABLE.map((entry) => entry.errorClass),
|
|
113
|
+
NO_ERROR_TEXT_CLASS,
|
|
114
|
+
];
|
|
101
115
|
export function elideImagePayloads(value) {
|
|
102
116
|
if (Array.isArray(value))
|
|
103
117
|
return value.map(elideImagePayloads);
|
|
@@ -270,7 +284,7 @@ export class SessionLog {
|
|
|
270
284
|
}
|
|
271
285
|
catch {
|
|
272
286
|
}
|
|
273
|
-
const errorClass = classifyError(entry.error);
|
|
287
|
+
const errorClass = entry.success ? null : (classifyError(entry.error) ?? NO_ERROR_TEXT_CLASS);
|
|
274
288
|
const captureResult = process.env.EDITMAMEI_LOG_RESULTS === '1' && result !== undefined
|
|
275
289
|
? this.sanitizeValue(elideImagePayloads(result))
|
|
276
290
|
: undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
const storage = new AsyncLocalStorage();
|
|
3
|
+
export function budgetContextFor(toolName, budgetMs, now = Date.now()) {
|
|
4
|
+
const ownDeadline = now + budgetMs;
|
|
5
|
+
const outer = storage.getStore();
|
|
6
|
+
const deadline = outer ? Math.min(ownDeadline, outer.deadline) : ownDeadline;
|
|
7
|
+
return { toolName, budgetMs, deadline };
|
|
8
|
+
}
|
|
9
|
+
export function runWithToolBudget(context, fn) {
|
|
10
|
+
return storage.run(context, fn);
|
|
11
|
+
}
|
|
12
|
+
export function currentToolBudget() {
|
|
13
|
+
return storage.getStore();
|
|
14
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.
|
|
1
|
+
export const VERSION = '1.4.0';
|
package/package.json
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editmamei",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Photoshop MCP server: natural-language AI photo editing with your own Photoshop (Community Edition)",
|
|
5
5
|
"mcpName": "io.github.editmamei/editmamei",
|
|
6
6
|
"editmamei": {
|
|
7
7
|
"fixesByVersion": {
|
|
8
|
+
"1.4.0": [
|
|
9
|
+
"ps_read_scene",
|
|
10
|
+
"ps_selection_channel",
|
|
11
|
+
"ps_select",
|
|
12
|
+
"ps_modify_selection"
|
|
13
|
+
],
|
|
8
14
|
"1.1.0": [
|
|
9
15
|
"ps_create_clipping_mask"
|
|
10
16
|
],
|
|
@@ -65,6 +71,6 @@
|
|
|
65
71
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
66
72
|
"adm-zip": "^0.6.0",
|
|
67
73
|
"jpeg-js": "^0.4.4",
|
|
68
|
-
"onnxruntime-web": "1.
|
|
74
|
+
"onnxruntime-web": "1.29.0"
|
|
69
75
|
}
|
|
70
76
|
}
|