editmamei 1.9.3 → 1.9.5
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/dist/backends/gimp/bridge/ops.py +29 -8
- package/dist/backends/gimp/session.js +4 -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/clients/shell.js +2 -1
- package/dist/cli/report.js +1 -1
- package/dist/core/preview-privacy.js +6 -8
- package/dist/core/server.js +7 -5
- package/dist/core/tool-registry.js +8 -1
- package/dist/delivery/client.js +8 -1
- package/dist/delivery/provision.js +4 -0
- package/dist/detection/sam-segmenter.js +35 -1
- package/dist/platform/windows-detector.js +3 -2
- package/dist/platform/windows-runner.js +3 -2
- package/dist/platform/windows-system-tool.js +5 -0
- package/dist/skills/editmamei-skill.zip +0 -0
- package/dist/tools/adjustment-tools.js +5 -5
- package/dist/tools/brush-tools.js +1 -1
- package/dist/tools/detection-tools.js +1 -1
- package/dist/tools/diagnostics-tools.js +2 -2
- package/dist/tools/document-tools.js +4 -4
- package/dist/tools/filter-tools.js +2 -2
- package/dist/tools/gimp-compose-tools.js +1 -1
- package/dist/tools/gimp-geometry-tools.js +2 -13
- package/dist/tools/gimp-selection-tools.js +9 -2
- package/dist/tools/gimp-shared.js +16 -1
- package/dist/tools/gimp-transform-layer-tools.js +4 -3
- package/dist/tools/gimp-verify-tools.js +10 -4
- package/dist/tools/guide-tools.js +2 -0
- package/dist/tools/history-tools.js +1 -1
- package/dist/tools/image-placement-tools.js +2 -2
- package/dist/tools/inspect-tools.js +3 -1
- package/dist/tools/path-tools.js +1 -1
- package/dist/tools/portrait-tools.js +1 -1
- package/dist/tools/preview-tools.js +1 -1
- package/dist/tools/scene-tools.js +3 -2
- package/dist/tools/selection-tools.js +3 -3
- package/dist/tools/shape-tools.js +1 -1
- package/dist/tools/sky-tools.js +2 -2
- package/dist/tools/smart-object-tools.js +8 -2
- package/dist/tools/transform-canvas-tools.js +2 -0
- package/dist/utils/local-path.js +15 -0
- package/dist/utils/validate.js +4 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -817,8 +817,11 @@ def op_open(args):
|
|
|
817
817
|
# 'indexed'", but nothing propagates to Python), so a bare call here would silently
|
|
818
818
|
# report `ok: true` with the ORIGINAL precision still in effect, misleading the
|
|
819
819
|
# caller into thinking their promotion applied when it didn't. Checking the return
|
|
820
|
-
# value turns that into a real, catchable failure.
|
|
821
|
-
|
|
820
|
+
# value turns that into a real, catchable failure. GIMP also returns `False` when the
|
|
821
|
+
# image already has the target precision (a 16-bit TIFF opened at '16'), and that is
|
|
822
|
+
# already the outcome asked for, so it is not attempted.
|
|
823
|
+
target = _PRECISION_ENUM[precision]
|
|
824
|
+
if img.get_precision() != target and not img.convert_precision(target):
|
|
822
825
|
raise lib.OpError(
|
|
823
826
|
'gimp_op_failed',
|
|
824
827
|
"precision promotion to %r failed for this image (base_type=%s); GIMP "
|
|
@@ -2254,22 +2257,40 @@ def _proxy_render(img, max_px):
|
|
|
2254
2257
|
return dup, unmirrored
|
|
2255
2258
|
|
|
2256
2259
|
|
|
2260
|
+
# Filters whose geometry is a fraction of their layer's own extent (`lib.build_vignette_params`):
|
|
2261
|
+
# a crop before they render would re-centre them on the cropped area.
|
|
2262
|
+
_EXTENT_RELATIVE_OPERATIONS = frozenset({'gegl:vignette'})
|
|
2263
|
+
|
|
2264
|
+
|
|
2257
2265
|
def _region_full_res(img, region):
|
|
2258
|
-
"""Full-res duplicate, cropped to `region
|
|
2266
|
+
"""Full-res duplicate, cropped to `region` and flattened. Caller deletes it. A plain
|
|
2259
2267
|
`Image.duplicate()` copies each layer's real live DrawableFilters (not just pixels), so
|
|
2260
|
-
rendering this way is exact -- the filter runs at native resolution
|
|
2261
|
-
|
|
2262
|
-
|
|
2268
|
+
rendering this way is exact -- the filter runs at native resolution BEFORE any later
|
|
2269
|
+
downscale, unlike the preview proxy (which shrinks the base first and then has to approximate
|
|
2270
|
+
a spatial filter's radius, see `_mirror_filters`).
|
|
2271
|
+
|
|
2272
|
+
The crop comes first, because a full-size flatten per region is far slower with heavy filters.
|
|
2273
|
+
An image carrying a filter in `_EXTENT_RELATIVE_OPERATIONS` is the exception: it is flattened
|
|
2274
|
+
at full size and cropped after, so the filter lands where the whole image shows it.
|
|
2263
2275
|
|
|
2264
2276
|
`region` is validated against `img`'s FULL-RES bounds before anything is duplicated (partly
|
|
2265
2277
|
or fully outside raises `invalid_argument` rather than silently clamping to whatever sliver
|
|
2266
2278
|
overlaps); the duplicate itself is torn down on ANY failure after that, not just the size
|
|
2267
2279
|
check, so a crop/flatten error can never leak an orphaned image."""
|
|
2268
2280
|
x, y, w, h = lib.validate_region(region, img.get_width(), img.get_height())
|
|
2281
|
+
extent_relative = any(
|
|
2282
|
+
f.get_operation_name() in _EXTENT_RELATIVE_OPERATIONS
|
|
2283
|
+
for l in _all_layers(img)
|
|
2284
|
+
for f in l.get_filters()
|
|
2285
|
+
)
|
|
2269
2286
|
dup = img.duplicate()
|
|
2270
2287
|
try:
|
|
2271
|
-
|
|
2272
|
-
|
|
2288
|
+
if extent_relative:
|
|
2289
|
+
layer = dup.flatten()
|
|
2290
|
+
dup.crop(w, h, x, y)
|
|
2291
|
+
else:
|
|
2292
|
+
dup.crop(w, h, x, y)
|
|
2293
|
+
layer = dup.flatten()
|
|
2273
2294
|
except Exception:
|
|
2274
2295
|
dup.delete()
|
|
2275
2296
|
raise
|
|
@@ -5,6 +5,7 @@ import { basename, dirname, join } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { Logger } from '../../utils/logger.js';
|
|
7
7
|
import { userOwnedTempRoot } from '../../utils/temp.js';
|
|
8
|
+
import { windowsSystemTool } from '../../platform/windows-system-tool.js';
|
|
8
9
|
import { GimpError, GIMP_ERROR_CODES } from './errors.js';
|
|
9
10
|
export const READY_TIMEOUT_MS = 180_000;
|
|
10
11
|
export const CALL_READY_WAIT_MS = 30_000;
|
|
@@ -45,7 +46,9 @@ export function defaultTreeKill(proc) {
|
|
|
45
46
|
if (hasExited(proc) || proc.pid === undefined)
|
|
46
47
|
return;
|
|
47
48
|
if (process.platform === 'win32') {
|
|
48
|
-
spawnSync('taskkill', ['/PID', String(proc.pid), '/T', '/F'], {
|
|
49
|
+
spawnSync(windowsSystemTool('taskkill.exe'), ['/PID', String(proc.pid), '/T', '/F'], {
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
});
|
|
49
52
|
return;
|
|
50
53
|
}
|
|
51
54
|
try {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { windowsSystemTool } from '../../platform/windows-system-tool.js';
|
|
2
3
|
export async function isOnPath(cmd) {
|
|
3
4
|
const probe = process.platform === 'win32'
|
|
4
|
-
? spawn('where', [cmd], { stdio: 'ignore', windowsHide: true })
|
|
5
|
+
? spawn(windowsSystemTool('where.exe'), [cmd], { stdio: 'ignore', windowsHide: true })
|
|
5
6
|
: spawn('command', ['-v', cmd], { stdio: 'ignore', shell: '/bin/sh' });
|
|
6
7
|
return new Promise((resolve) => {
|
|
7
8
|
const finish = (ok) => {
|
package/dist/cli/report.js
CHANGED
|
@@ -6,6 +6,6 @@ export async function runReport(opts = {}) {
|
|
|
6
6
|
const bundle = await collect({ note: opts.note });
|
|
7
7
|
const { path } = await write(bundle);
|
|
8
8
|
out(`\n Wrote an anonymized diagnostic bundle to:\n ${path}\n\n`);
|
|
9
|
-
out(` It contains recent logs + system info — no images, no full file paths, no tool arguments.\n`);
|
|
9
|
+
out(` It contains recent logs + system info — no images, no full file paths, and no tool arguments outside error messages.\n`);
|
|
10
10
|
out(` Attach this file to a new issue at ${ISSUES_URL} so the maintainers can debug.\n\n`);
|
|
11
11
|
}
|
|
@@ -40,15 +40,13 @@ export function withholdImages(result) {
|
|
|
40
40
|
if (counter.n === 0)
|
|
41
41
|
return result;
|
|
42
42
|
const what = counter.n === 1 ? 'An image was' : `${counter.n} images were`;
|
|
43
|
+
const text = `${what} withheld because the user's privacy.send_previews_to_llm setting is off. ` +
|
|
44
|
+
'Work from the text and numbers; ask the user if you need to see the image.';
|
|
43
45
|
return {
|
|
44
46
|
...stripped,
|
|
45
|
-
content: [
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
text: `${what} withheld because the user's privacy.send_previews_to_llm setting is off. ` +
|
|
50
|
-
'Work from the text and numbers; ask the user if you need to see the image.',
|
|
51
|
-
},
|
|
52
|
-
],
|
|
47
|
+
content: [...(stripped.content ?? []), { type: 'text', text }],
|
|
48
|
+
...(stripped.structuredContent
|
|
49
|
+
? { structuredContent: { ...stripped.structuredContent, images_withheld: text } }
|
|
50
|
+
: {}),
|
|
53
51
|
};
|
|
54
52
|
}
|
package/dist/core/server.js
CHANGED
|
@@ -61,10 +61,12 @@ function hashRetryKey(tool, args) {
|
|
|
61
61
|
export const FIRST_RUN_DISCLOSURE = 'First run: Editmamei collects content-free usage telemetry (tool name, ' +
|
|
62
62
|
'success, duration, bytes returned, version/edition/OS/PS-version, install channel, ' +
|
|
63
63
|
'which AI client connected, Node/OS/architecture versions, and per-session counts like ' +
|
|
64
|
-
'edits made and retries) to find what breaks.
|
|
65
|
-
'or tool arguments. The only identifier is a random install ID. Opt out
|
|
66
|
-
'`editmamei config set telemetry.usage false` (or edit
|
|
67
|
-
'Opt in to sanitized diagnostics
|
|
64
|
+
'edits made and retries) to find what breaks. Usage telemetry never sends image content, ' +
|
|
65
|
+
'file paths, or tool arguments. The only identifier is a random install ID. Opt out ' +
|
|
66
|
+
'anytime: `editmamei config set telemetry.usage false` (or edit ' +
|
|
67
|
+
'~/.editmamei/settings.json). Opt in to sanitized diagnostics (error messages, which can ' +
|
|
68
|
+
'name a layer or repeat a value you passed): ' +
|
|
69
|
+
'`editmamei config set telemetry.diagnostics true`.';
|
|
68
70
|
export const BACKGROUND_VERSION_PROBE_TIMEOUT_MS = 3_000;
|
|
69
71
|
const PING_ROUND_TRIP_CLASSES = new Set([
|
|
70
72
|
'timeout',
|
|
@@ -265,7 +267,7 @@ export class EditmameiServer {
|
|
|
265
267
|
this.toolRegistry.register('ps_ping', {
|
|
266
268
|
tool: {
|
|
267
269
|
name: 'ps_ping',
|
|
268
|
-
description: 'Test connection to Photoshop and report session-start discovery signals. Read-only and idempotent. Call this once at the start of a session to confirm liveness before invoking any other tool. **If the user has given you an open-ended editing task, ALSO call `ps_overview` after this** — it returns the workflow contract + capabilities map + verification primitives so you can plan well. Returns `version` (folds the removed get-version tool), `custom_action_sets` (count of Action Sets loaded in the Photoshop Actions palette — non-zero means recorded workflows exist; if action tools are available in this build they will appear in `tools/list`), `user_templates` (count of saved Editmamei templates), and `open_documents` (names of documents already open in Photoshop, so the LLM
|
|
270
|
+
description: 'Test connection to Photoshop and report session-start discovery signals. Read-only and idempotent. Call this once at the start of a session to confirm liveness before invoking any other tool. **If the user has given you an open-ended editing task, ALSO call `ps_overview` after this** — it returns the workflow contract + capabilities map + verification primitives so you can plan well. Returns `version` (folds the removed get-version tool), `custom_action_sets` (count of Action Sets loaded in the Photoshop Actions palette — non-zero means recorded workflows exist; if action tools are available in this build they will appear in `tools/list`), `user_templates` (count of saved Editmamei templates), and `open_documents` (names of documents already open in Photoshop, so the LLM can see which document to target).',
|
|
269
271
|
inputSchema: {
|
|
270
272
|
type: 'object',
|
|
271
273
|
properties: {},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Logger } from '../utils/logger.js';
|
|
2
2
|
import { getToolTimeoutMs } from '../utils/operation-timeouts.js';
|
|
3
3
|
import { budgetContextFor, runWithToolBudget } from '../utils/tool-budget-context.js';
|
|
4
|
+
import { missingRequiredArg } from '../utils/validate.js';
|
|
4
5
|
export class ToolRegistry {
|
|
5
6
|
logger;
|
|
6
7
|
tools;
|
|
@@ -48,7 +49,13 @@ export class ToolRegistry {
|
|
|
48
49
|
let error;
|
|
49
50
|
let result;
|
|
50
51
|
try {
|
|
51
|
-
|
|
52
|
+
const missing = missingRequiredArg(definition.tool.inputSchema, args);
|
|
53
|
+
result = missing
|
|
54
|
+
? {
|
|
55
|
+
content: [{ type: 'text', text: `Missing required argument: ${missing}` }],
|
|
56
|
+
isError: true,
|
|
57
|
+
}
|
|
58
|
+
: await runWithToolBudget(budgetContextFor(name, getToolTimeoutMs(name)), () => definition.handler(args));
|
|
52
59
|
if (result?.isError) {
|
|
53
60
|
success = false;
|
|
54
61
|
const firstText = result.content?.find((c) => c.type === 'text');
|
package/dist/delivery/client.js
CHANGED
|
@@ -14,6 +14,7 @@ export class DeliveryError extends Error {
|
|
|
14
14
|
const MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
|
|
15
15
|
const USER_AGENT = 'editmamei-delivery-client/1';
|
|
16
16
|
const HEADER_KEY = 'x-editmamei-license-key';
|
|
17
|
+
const HEADER_ACTIVATION = 'x-editmamei-activation-id';
|
|
17
18
|
const defaultFetch = (url, init) => fetch(url, init);
|
|
18
19
|
const DEFAULT_RETRY = { attempts: 4, baseDelayMs: 1000, maxDelayMs: 65_000 };
|
|
19
20
|
const DEFAULT_MIN_INTERVAL_MS = 500;
|
|
@@ -25,6 +26,7 @@ export class DeliveryClient {
|
|
|
25
26
|
sleep;
|
|
26
27
|
minIntervalMs;
|
|
27
28
|
maxArtifactBytes;
|
|
29
|
+
activationId;
|
|
28
30
|
lastRequestAt = 0;
|
|
29
31
|
constructor(opts = {}) {
|
|
30
32
|
this.cfg = opts.config ?? resolveDeliveryConfig();
|
|
@@ -33,6 +35,7 @@ export class DeliveryClient {
|
|
|
33
35
|
this.sleep = opts.sleep ?? defaultSleep;
|
|
34
36
|
this.minIntervalMs = opts.minRequestIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
35
37
|
this.maxArtifactBytes = opts.maxArtifactBytes ?? MAX_ARTIFACT_BYTES;
|
|
38
|
+
this.activationId = opts.activationId || undefined;
|
|
36
39
|
if (!this.cfg.baseUrl) {
|
|
37
40
|
throw new DeliveryError('Delivery endpoint is not configured. Set EDITMAMEI_DELIVERY_URL ' +
|
|
38
41
|
'(e.g. http://localhost:8787 for a local wrangler dev Worker).', 0, 'not_configured');
|
|
@@ -130,7 +133,11 @@ export class DeliveryClient {
|
|
|
130
133
|
try {
|
|
131
134
|
return await this.fetchImpl(`${this.cfg.baseUrl}${path}`, {
|
|
132
135
|
method: 'GET',
|
|
133
|
-
headers: {
|
|
136
|
+
headers: {
|
|
137
|
+
'user-agent': USER_AGENT,
|
|
138
|
+
[HEADER_KEY]: key,
|
|
139
|
+
...(this.activationId ? { [HEADER_ACTIVATION]: this.activationId } : {}),
|
|
140
|
+
},
|
|
134
141
|
});
|
|
135
142
|
}
|
|
136
143
|
catch (err) {
|
|
@@ -3,6 +3,7 @@ import { sha256Hex } from './crypto.js';
|
|
|
3
3
|
import { verifyModuleSignature, verifyModuleSignatureV2, digestsRootSha256Hex, isModuleFileDigestArray, } from './signing.js';
|
|
4
4
|
import { installModule, readInstalledModule } from './store.js';
|
|
5
5
|
import { KERNEL_ABI } from '../kernel/host-api.js';
|
|
6
|
+
import { readLicense } from '../license/store.js';
|
|
6
7
|
import { Logger } from '../utils/logger.js';
|
|
7
8
|
const logger = new Logger('Modules');
|
|
8
9
|
const SKU_RE = /^[a-z0-9-]{2,32}$/;
|
|
@@ -15,12 +16,15 @@ export async function provisionModules(key, opts = {}) {
|
|
|
15
16
|
errors: [],
|
|
16
17
|
notConfigured: false,
|
|
17
18
|
};
|
|
19
|
+
const license = readLicense(opts);
|
|
20
|
+
const activationId = license?.key === key ? license.activation_id : undefined;
|
|
18
21
|
let client;
|
|
19
22
|
try {
|
|
20
23
|
client = new DeliveryClient({
|
|
21
24
|
config: opts.config,
|
|
22
25
|
fetchImpl: opts.fetchImpl,
|
|
23
26
|
sleep: opts.sleep,
|
|
27
|
+
activationId,
|
|
24
28
|
});
|
|
25
29
|
}
|
|
26
30
|
catch (e) {
|
|
@@ -63,9 +63,43 @@ function sameImageKey(a, b) {
|
|
|
63
63
|
a.byteLength === b.byteLength &&
|
|
64
64
|
a.hash === b.hash);
|
|
65
65
|
}
|
|
66
|
+
export function toSamFrame(img) {
|
|
67
|
+
const long = Math.max(img.width, img.height);
|
|
68
|
+
if (long === SAM_INPUT)
|
|
69
|
+
return img;
|
|
70
|
+
const scale = SAM_INPUT / long;
|
|
71
|
+
const width = Math.max(1, Math.round(img.width * scale));
|
|
72
|
+
const height = Math.max(1, Math.round(img.height * scale));
|
|
73
|
+
const data = new Uint8Array(width * height * 4);
|
|
74
|
+
for (let y = 0; y < height; y++) {
|
|
75
|
+
const sy = Math.min(Math.max(((y + 0.5) * img.height) / height - 0.5, 0), img.height - 1);
|
|
76
|
+
const y0 = Math.floor(sy);
|
|
77
|
+
const y1 = Math.min(y0 + 1, img.height - 1);
|
|
78
|
+
const fy = sy - y0;
|
|
79
|
+
for (let x = 0; x < width; x++) {
|
|
80
|
+
const sx = Math.min(Math.max(((x + 0.5) * img.width) / width - 0.5, 0), img.width - 1);
|
|
81
|
+
const x0 = Math.floor(sx);
|
|
82
|
+
const x1 = Math.min(x0 + 1, img.width - 1);
|
|
83
|
+
const fx = sx - x0;
|
|
84
|
+
for (let c = 0; c < 4; c++) {
|
|
85
|
+
const top = img.data[(y0 * img.width + x0) * 4 + c] * (1 - fx) +
|
|
86
|
+
img.data[(y0 * img.width + x1) * 4 + c] * fx;
|
|
87
|
+
const bottom = img.data[(y1 * img.width + x0) * 4 + c] * (1 - fx) +
|
|
88
|
+
img.data[(y1 * img.width + x1) * 4 + c] * fx;
|
|
89
|
+
data[(y * width + x) * 4 + c] = Math.round(top * (1 - fy) + bottom * fy);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { width, height, data };
|
|
94
|
+
}
|
|
95
|
+
export function encoderInputOf(img) {
|
|
96
|
+
const framed = toSamFrame(img);
|
|
97
|
+
return { data: toEncoderInput(framed), dims: [framed.height, framed.width, 3] };
|
|
98
|
+
}
|
|
66
99
|
async function realRunEncoder(img) {
|
|
67
100
|
const session = await loadModel(resolveModelPath(MODEL_ENCODER));
|
|
68
|
-
const
|
|
101
|
+
const { data, dims } = encoderInputOf(img);
|
|
102
|
+
const input = new ort.Tensor('float32', data, dims);
|
|
69
103
|
const out = await session.run({ input_image: input });
|
|
70
104
|
return out.image_embeddings;
|
|
71
105
|
}
|
|
@@ -2,6 +2,7 @@ import { exec } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { access, constants } from 'fs/promises';
|
|
4
4
|
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { windowsSystemTool } from './windows-system-tool.js';
|
|
5
6
|
const execAsync = promisify(exec);
|
|
6
7
|
export class WindowsDetector {
|
|
7
8
|
logger = new Logger('WindowsDetector');
|
|
@@ -35,7 +36,7 @@ export class WindowsDetector {
|
|
|
35
36
|
];
|
|
36
37
|
for (const key of productKeys) {
|
|
37
38
|
try {
|
|
38
|
-
const { stdout } = await execAsync(`reg query "${key}" /s`);
|
|
39
|
+
const { stdout } = await execAsync(`"${windowsSystemTool('reg.exe')}" query "${key}" /s`);
|
|
39
40
|
const entries = this.readRegistryEntries(stdout);
|
|
40
41
|
if (entries.length > 0) {
|
|
41
42
|
const newest = entries.sort((a, b) => b.version.localeCompare(a.version))[0];
|
|
@@ -54,7 +55,7 @@ export class WindowsDetector {
|
|
|
54
55
|
];
|
|
55
56
|
for (const key of comRegistrations) {
|
|
56
57
|
try {
|
|
57
|
-
const { stdout } = await execAsync(`reg query "${key}" /ve`);
|
|
58
|
+
const { stdout } = await execAsync(`"${windowsSystemTool('reg.exe')}" query "${key}" /ve`);
|
|
58
59
|
const exePath = this.readComServerPath(stdout);
|
|
59
60
|
if (exePath) {
|
|
60
61
|
const info = await this.inspectPath(exePath);
|
|
@@ -7,6 +7,7 @@ import { ScriptQueue } from './script-queue.js';
|
|
|
7
7
|
import { runChildWithTimeout } from './run-child.js';
|
|
8
8
|
import { decodeScriptResult } from './script-result.js';
|
|
9
9
|
import { waitForLaunchReady } from './launch-readiness.js';
|
|
10
|
+
import { windowsSystemTool } from './windows-system-tool.js';
|
|
10
11
|
const execAsync = promisify(exec);
|
|
11
12
|
const LAUNCH_PROBE_TIMEOUT_MS = 2_000;
|
|
12
13
|
const IS_RUNNING_EXEC_TIMEOUT_MS = 3_000;
|
|
@@ -21,7 +22,7 @@ export class WindowsScriptRunner {
|
|
|
21
22
|
try {
|
|
22
23
|
const scriptPath = await dir.write('script.jsx', script);
|
|
23
24
|
const shimPath = await dir.write('run.vbs', this.buildComShim(scriptPath));
|
|
24
|
-
const { stdout, stderr } = await runChildWithTimeout('cscript', ['//nologo', shimPath], {
|
|
25
|
+
const { stdout, stderr } = await runChildWithTimeout(windowsSystemTool('cscript.exe'), ['//nologo', shimPath], {
|
|
25
26
|
timeout: timeoutMs,
|
|
26
27
|
diagLabel: 'cscript run.vbs',
|
|
27
28
|
});
|
|
@@ -61,7 +62,7 @@ End If
|
|
|
61
62
|
}
|
|
62
63
|
async isRunning() {
|
|
63
64
|
try {
|
|
64
|
-
const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq Photoshop.exe"
|
|
65
|
+
const { stdout } = await execAsync(`"${windowsSystemTool('tasklist.exe')}" /FI "IMAGENAME eq Photoshop.exe"`, {
|
|
65
66
|
timeout: IS_RUNNING_EXEC_TIMEOUT_MS,
|
|
66
67
|
});
|
|
67
68
|
return stdout.toLowerCase().includes('photoshop.exe');
|
|
Binary file
|
|
@@ -149,9 +149,9 @@ const addAdjustmentLayerSchema = {
|
|
|
149
149
|
},
|
|
150
150
|
bw_tint_hue: {
|
|
151
151
|
type: 'number',
|
|
152
|
-
description: 'For type=black_and_white when bw_tint=true. Tint hue in degrees (
|
|
152
|
+
description: 'For type=black_and_white when bw_tint=true. Tint hue in degrees (0 to 360; negative values wrap). Default 220 (cool blue).',
|
|
153
153
|
minimum: -180,
|
|
154
|
-
maximum:
|
|
154
|
+
maximum: 360,
|
|
155
155
|
default: 220,
|
|
156
156
|
},
|
|
157
157
|
bw_tint_saturation: {
|
|
@@ -475,7 +475,7 @@ const addAdjustmentLayerSchema = {
|
|
|
475
475
|
},
|
|
476
476
|
exp_gamma: {
|
|
477
477
|
type: 'number',
|
|
478
|
-
description: 'For type=exposure. Gamma correction (0.01 to 9.99). PS default 1.0 (identity). Values
|
|
478
|
+
description: 'For type=exposure. Gamma correction (0.01 to 9.99). PS default 1.0 (identity). Values >1 brighten midtones; <1 darken them.',
|
|
479
479
|
minimum: 0.01,
|
|
480
480
|
maximum: 9.99,
|
|
481
481
|
default: 1.0,
|
|
@@ -622,7 +622,7 @@ const APPLY_ADJUSTMENT_INPUT_SCHEMA = {
|
|
|
622
622
|
type: {
|
|
623
623
|
type: 'string',
|
|
624
624
|
enum: [...APPLY_ADJUSTMENT_TYPES],
|
|
625
|
-
description: 'Which destructive adjustment to bake
|
|
625
|
+
description: 'Which destructive adjustment to bake. shadows_highlights and equalize have no adjustment layer in PS; color_lookup does, and this bakes it instead. ' +
|
|
626
626
|
'shadows_highlights: one-pass shadow/highlight recovery (shadow_amount, highlight_amount, …). ' +
|
|
627
627
|
'equalize: parameter-free histogram equalization. ' +
|
|
628
628
|
'color_lookup: bake a 3DLUT grade — cl_lut_name REQUIRED. ' +
|
|
@@ -639,7 +639,7 @@ export function createAdjustmentTools(connection, snippetClient) {
|
|
|
639
639
|
{
|
|
640
640
|
tool: {
|
|
641
641
|
name: 'ps_apply_adjustment',
|
|
642
|
-
description: "Apply a DESTRUCTIVE tonal adjustment
|
|
642
|
+
description: "Apply a DESTRUCTIVE tonal adjustment, baked into pixels — chosen via `type`. Runs on a DUPLICATE of the active layer by default (auto-duplicate-first — the original is preserved; revert by deleting the copy); pass `apply_to_active_layer: true` to bake in place. Auto-rasterizes text/smart-object layers. `shadows_highlights` recovers blown highlights + crushed shadows in one pass (defaults match Adobe's dialog: 35 shadow amount, +20 color correction); `equalize` stretches/flattens the histogram (parameter-free); `color_lookup` bakes a 3DLUT grade (cl_lut_name required — leaf name of a file in Presets/3DLUTs/ or an absolute .cube/.3dl/.look path). For EDITABLE tonal/color work, prefer ps_add_adjustment_layer.",
|
|
643
643
|
inputSchema: APPLY_ADJUSTMENT_INPUT_SCHEMA,
|
|
644
644
|
outputSchema: {
|
|
645
645
|
type: 'object',
|
|
@@ -16,7 +16,7 @@ const applyBrushStrokeSchema = {
|
|
|
16
16
|
placement: {
|
|
17
17
|
...PLACEMENT_SCHEMA,
|
|
18
18
|
description: 'ANCHOR-RELATIONAL stroke path (preferred over supplying pixels): a PATH relation — `along` a traced edge ' +
|
|
19
|
-
'or a
|
|
19
|
+
'or a face-mesh landmark curve, where available, `offset-curve`, or `segment` between two anchors — so the stroke traces ' +
|
|
20
20
|
'the resolved, gate-verified curve (the FULL polyline, not just endpoints: paint along the jaw / horizon / ' +
|
|
21
21
|
'under-eye). Strokes ONLY if the gate PASSES. When set, `path` is ignored; tool/brush_size/source_point/' +
|
|
22
22
|
'colors/dynamics/jitter still apply. See ps_resolve_placement for the anchors + relation vocabulary.',
|
|
@@ -174,7 +174,7 @@ export function createDetectionTools(connection, _snippetClient, client = new On
|
|
|
174
174
|
{
|
|
175
175
|
tool: {
|
|
176
176
|
name: 'ps_detect',
|
|
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.
|
|
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. Detection runs locally on this device; annotate:true returns a JPEG to your AI client. 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]. Pass `annotate:true` for an annotated preview JPEG (faces cyan, objects magenta) when you need to visually confirm a surprising result.',
|
|
178
178
|
inputSchema: detectSchema,
|
|
179
179
|
outputSchema: {
|
|
180
180
|
type: 'object',
|
|
@@ -15,7 +15,7 @@ export function createDiagnosticsTools(_connection, _snippetClient, deps = {}) {
|
|
|
15
15
|
{
|
|
16
16
|
tool: {
|
|
17
17
|
name: 'ps_report_problem',
|
|
18
|
-
description: "Collect an ANONYMIZED diagnostic bundle and write it to the user's Downloads folder so they can attach it to a bug report — use when Editmamei misbehaves (won't connect, a tool keeps failing, unexpected results). The bundle holds recent server logs, system info (Editmamei/OS/Photoshop versions), and a content-free summary of recent tool calls (name, success, duration, error class). It contains NO image content, NO tool arguments, and file paths reduced to basenames. Does not touch Photoshop; writes one JSON file. After calling, tell the user the file path and that they can attach it to a new issue at " +
|
|
18
|
+
description: "Collect an ANONYMIZED diagnostic bundle and write it to the user's Downloads folder so they can attach it to a bug report — use when Editmamei misbehaves (won't connect, a tool keeps failing, unexpected results). The bundle holds recent server logs, system info (Editmamei/OS/Photoshop versions), and a content-free summary of recent tool calls (name, success, duration, error class). It contains NO image content, the tool-call summary carries NO tool arguments, and file paths are reduced to basenames; log lines can still quote a layer name or a value from an error message. Does not touch Photoshop; writes one JSON file. After calling, tell the user the file path and that they can attach it to a new issue at " +
|
|
19
19
|
ISSUES_URL +
|
|
20
20
|
'.',
|
|
21
21
|
inputSchema: reportSchema,
|
|
@@ -47,7 +47,7 @@ export function createDiagnosticsTools(_connection, _snippetClient, deps = {}) {
|
|
|
47
47
|
{
|
|
48
48
|
type: 'text',
|
|
49
49
|
text: `Wrote an anonymized diagnostic bundle to:\n ${path}\n\n` +
|
|
50
|
-
`It contains recent logs + system info — no images, no full file paths, no tool arguments. ` +
|
|
50
|
+
`It contains recent logs + system info — no images, no full file paths, and no tool arguments outside error messages. ` +
|
|
51
51
|
`Attach this file to a new issue at ${ISSUES_URL} so the maintainers can debug.`,
|
|
52
52
|
},
|
|
53
53
|
],
|
|
@@ -3,7 +3,7 @@ import { runScript } from '../utils/run-script.js';
|
|
|
3
3
|
import { validateArgs } from '../utils/validate.js';
|
|
4
4
|
import { OPEN_DOCUMENT_TIMEOUT_MS, OPEN_DOCUMENT_REPROBE_TIMEOUT_MS, SAVE_PSD_REPROBE_TIMEOUT_MS, getToolTimeoutMs, } from '../utils/operation-timeouts.js';
|
|
5
5
|
import { toolErrorResult, runSnippetTool } from '../utils/tool-helpers.js';
|
|
6
|
-
import { checkOutputPath, recordOutputPath,
|
|
6
|
+
import { checkOutputPath, recordOutputPath, requireLocalImagePath } from '../utils/local-path.js';
|
|
7
7
|
import { purgeSceneChannels } from '../perception/region-precompute.js';
|
|
8
8
|
import { Logger } from '../utils/logger.js';
|
|
9
9
|
const logger = new Logger('document-tools');
|
|
@@ -136,7 +136,7 @@ const exportJpegSchema = {
|
|
|
136
136
|
},
|
|
137
137
|
quality: {
|
|
138
138
|
type: 'integer',
|
|
139
|
-
description: "JPEG quality 0-100 (the Photoshop '
|
|
139
|
+
description: "JPEG quality 0-100 (the Photoshop 'Export As' scale). 100 = maximum. Mapped internally to Photoshop's 0-12 JPEGSaveOptions scripting scale (e.g. 90→11, 100→12).",
|
|
140
140
|
default: 90,
|
|
141
141
|
minimum: 0,
|
|
142
142
|
maximum: 100,
|
|
@@ -375,7 +375,7 @@ export function createDocumentTools(connection, snippetClient) {
|
|
|
375
375
|
{
|
|
376
376
|
tool: {
|
|
377
377
|
name: 'ps_export',
|
|
378
|
-
description: 'Export the active document to a flattened image file — JPEG or PNG, chosen via `format`. Operates on a duplicate, so the working document is unchanged. Optionally downscales so the long edge equals long_edge_px. JPEG: quality 0-100 (
|
|
378
|
+
description: 'Export the active document to a flattened image file — JPEG or PNG, chosen via `format`. Operates on a duplicate, so the working document is unchanged. Optionally downscales so the long edge equals long_edge_px. JPEG: quality 0-100 (Export As scale), optional sRGB convert + profile embed. PNG: transparent background (alpha preserved) or flattened onto white, compression 0-9.',
|
|
379
379
|
inputSchema: exportInputSchema,
|
|
380
380
|
outputSchema: {
|
|
381
381
|
type: 'object',
|
|
@@ -563,7 +563,7 @@ async function openDocumentPipeline(connection, snippetClient, rawArgs) {
|
|
|
563
563
|
let filePath;
|
|
564
564
|
try {
|
|
565
565
|
const args = validateArgs(openDocumentSchema, rawArgs);
|
|
566
|
-
filePath =
|
|
566
|
+
filePath = requireLocalImagePath('file_path', args.file_path);
|
|
567
567
|
const suppressDialogs = args.suppress_dialogs;
|
|
568
568
|
const bitDepth = args.bit_depth;
|
|
569
569
|
const script = await snippetClient.build('openDocumentPipeline', {
|
|
@@ -3,7 +3,7 @@ import { validateArgs } from '../utils/validate.js';
|
|
|
3
3
|
import { OnnxLandmarkDetectionClient } from '../detection/landmark-detection-client.js';
|
|
4
4
|
import { resolveGatedPlacement, PLACEMENT_SCHEMA } from '../perception/grounding-locate.js';
|
|
5
5
|
import { toolErrorResult, runSnippetTool, applyToActiveLayerProp, asSmartFilterProp, unknownDiscriminator, } from '../utils/tool-helpers.js';
|
|
6
|
-
import {
|
|
6
|
+
import { requireLocalImagePath } from '../utils/local-path.js';
|
|
7
7
|
import { LAYER_BLEND_MODES } from '../utils/blend-modes.js';
|
|
8
8
|
import { runSmartFilterOp } from './smart-object-tools.js';
|
|
9
9
|
const gaussianBlurSchema = {
|
|
@@ -1653,7 +1653,7 @@ async function applyDisplace(connection, snippetClient, rawArgs) {
|
|
|
1653
1653
|
snippet: 'applyDisplace',
|
|
1654
1654
|
errorPrefix: 'Error applying Displace',
|
|
1655
1655
|
params: (args) => ({
|
|
1656
|
-
mapPath:
|
|
1656
|
+
mapPath: requireLocalImagePath('map_path', args.map_path),
|
|
1657
1657
|
horizontalScale: args.horizontal_scale ?? 10,
|
|
1658
1658
|
verticalScale: args.vertical_scale ?? 10,
|
|
1659
1659
|
displacementMap: args.displacement_map ?? 'stretch_to_fit',
|
|
@@ -195,7 +195,7 @@ const canvasSchema = {
|
|
|
195
195
|
'pre-existing transparency in those layers. On a GRAYSCALE image a hex color renders as ' +
|
|
196
196
|
'its LUMINANCE (a weighted gray, not one channel picked out). transparent: genuinely ' +
|
|
197
197
|
'transparent only in a live, layered document (e.g. saved via gimp_save_xcf) — ' +
|
|
198
|
-
'gimp_export and gimp_layer op=flatten
|
|
198
|
+
'gimp_export (unless keep_alpha or a layered psd) and gimp_layer op=flatten flatten first, which fills any transparent ' +
|
|
199
199
|
"area with GIMP's ambient background color (white, unchanged by this tool) and drops " +
|
|
200
200
|
'alpha, not with transparency. A non-transparent fill on an INDEXED image is REFUSED ' +
|
|
201
201
|
'outright, before the canvas resizes at all — indexed has no backdrop layer type to add.',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateArgs } from '../utils/validate.js';
|
|
2
2
|
import { toolGimpErrorResult, unknownDiscriminator } from '../utils/tool-helpers.js';
|
|
3
|
-
import { GIMP_IMAGE_PROP, runGimpTool, pickSchemaDeclaredKeys } from './gimp-shared.js';
|
|
3
|
+
import { GIMP_IMAGE_PROP, effectUpdateFailuresNote, runGimpTool, pickSchemaDeclaredKeys, withWarning, } from './gimp-shared.js';
|
|
4
4
|
const cropSchema = {
|
|
5
5
|
type: 'object',
|
|
6
6
|
properties: {
|
|
@@ -75,17 +75,6 @@ const transformCanvasSchema = {
|
|
|
75
75
|
},
|
|
76
76
|
required: ['image', 'op'],
|
|
77
77
|
};
|
|
78
|
-
export function effectUpdateFailuresNote(failures) {
|
|
79
|
-
if (!failures || failures.length === 0)
|
|
80
|
-
return '';
|
|
81
|
-
const plural = failures.length > 1;
|
|
82
|
-
const it = plural ? 'them' : 'it';
|
|
83
|
-
return (` WARNING: ${failures.join(', ')} could not be updated for this transform and ` +
|
|
84
|
-
`${plural ? 'were' : 'was'} left at ${plural ? 'their' : 'its'} old position/angle/offset/size ` +
|
|
85
|
-
`instead -- delete ${it} (gimp_filter op=delete) and re-create ${it} if ${plural ? 'they need' : 'it needs'} ` +
|
|
86
|
-
`to track the content (re-applying THIS SAME transform again would move ${it} a second time, ` +
|
|
87
|
-
`misaligning ${it} the other way).`);
|
|
88
|
-
}
|
|
89
78
|
async function gimpTransformCanvas(gimp, rawArgs) {
|
|
90
79
|
try {
|
|
91
80
|
const args = validateArgs(transformCanvasSchema, rawArgs);
|
|
@@ -100,7 +89,7 @@ async function gimpTransformCanvas(gimp, rawArgs) {
|
|
|
100
89
|
effectUpdateFailuresNote(result.effect_update_failures);
|
|
101
90
|
return {
|
|
102
91
|
content: [{ type: 'text', text }],
|
|
103
|
-
structuredContent: result,
|
|
92
|
+
structuredContent: withWarning(result, effectUpdateFailuresNote(result.effect_update_failures)),
|
|
104
93
|
};
|
|
105
94
|
}
|
|
106
95
|
catch (error) {
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { rm } from 'node:fs/promises';
|
|
3
3
|
import { validateArgs } from '../utils/validate.js';
|
|
4
4
|
import { toolGimpErrorResult } from '../utils/tool-helpers.js';
|
|
5
|
-
import { GIMP_IMAGE_PROP, GIMP_LAYER_ID_PROP, GIMP_LAYER_PROP, GIMP_MAX_PX_PROP, readRender, defaultPreviewsAllowed, pickSchemaDeclaredKeys, runGimpTool, } from './gimp-shared.js';
|
|
5
|
+
import { GIMP_IMAGE_PROP, GIMP_LAYER_ID_PROP, GIMP_LAYER_PROP, GIMP_MAX_PX_PROP, readRender, defaultPreviewsAllowed, pickSchemaDeclaredKeys, runGimpTool, withWarning, } from './gimp-shared.js';
|
|
6
6
|
const maskResultSchema = {
|
|
7
7
|
type: 'object',
|
|
8
8
|
properties: {
|
|
@@ -274,7 +274,14 @@ async function selectionPreview(gimp, rawArgs, previewsAllowedFn) {
|
|
|
274
274
|
: ' privacy.send_previews_to_llm is false — image not returned to the model.') +
|
|
275
275
|
unmirroredNote,
|
|
276
276
|
});
|
|
277
|
-
return {
|
|
277
|
+
return {
|
|
278
|
+
content,
|
|
279
|
+
structuredContent: withWarning({
|
|
280
|
+
width: r.width,
|
|
281
|
+
height: r.height,
|
|
282
|
+
...(r.unmirrored_filters?.length ? { unmirrored_filters: r.unmirrored_filters } : {}),
|
|
283
|
+
}, unmirroredNote),
|
|
284
|
+
};
|
|
278
285
|
}
|
|
279
286
|
finally {
|
|
280
287
|
await rm(out, { force: true }).catch(() => undefined);
|
|
@@ -54,6 +54,21 @@ export async function readRender(path) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
export const defaultPreviewsAllowed = () => loadSettings().settings.privacy.send_previews_to_llm;
|
|
57
|
+
export function effectUpdateFailuresNote(failures) {
|
|
58
|
+
if (!failures || failures.length === 0)
|
|
59
|
+
return '';
|
|
60
|
+
const plural = failures.length > 1;
|
|
61
|
+
const it = plural ? 'them' : 'it';
|
|
62
|
+
return (` WARNING: ${failures.join(', ')} could not be updated for this transform and ` +
|
|
63
|
+
`${plural ? 'were' : 'was'} left at ${plural ? 'their' : 'its'} old position/angle/offset/size ` +
|
|
64
|
+
`instead -- delete ${it} (gimp_filter op=delete) and re-create ${it} if ${plural ? 'they need' : 'it needs'} ` +
|
|
65
|
+
`to track the content (re-applying THIS SAME transform again would move ${it} a second time, ` +
|
|
66
|
+
`misaligning ${it} the other way).`);
|
|
67
|
+
}
|
|
68
|
+
export function withWarning(structured, note) {
|
|
69
|
+
const warning = note.trim();
|
|
70
|
+
return warning ? { ...structured, warning } : structured;
|
|
71
|
+
}
|
|
57
72
|
export async function runGimpTool(spec) {
|
|
58
73
|
try {
|
|
59
74
|
const args = validateArgs(spec.schema, spec.rawArgs);
|
|
@@ -61,7 +76,7 @@ export async function runGimpTool(spec) {
|
|
|
61
76
|
const result = await spec.gimp.call(spec.op, bridgeArgs);
|
|
62
77
|
return {
|
|
63
78
|
content: [{ type: 'text', text: spec.successText(result, args) }],
|
|
64
|
-
structuredContent: result,
|
|
79
|
+
structuredContent: withWarning(result, effectUpdateFailuresNote(result.effect_update_failures)),
|
|
65
80
|
};
|
|
66
81
|
}
|
|
67
82
|
catch (error) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { validateArgs } from '../utils/validate.js';
|
|
2
2
|
import { toolGimpErrorResult, unknownDiscriminator } from '../utils/tool-helpers.js';
|
|
3
3
|
import { GIMP_IMAGE_PROP, GIMP_LAYER_PROP, GIMP_LAYER_ID_PROP, pickSchemaDeclaredKeys, } from './gimp-shared.js';
|
|
4
|
-
import { EFFECT_UPDATE_FAILURES_PROP
|
|
4
|
+
import { EFFECT_UPDATE_FAILURES_PROP } from './gimp-geometry-tools.js';
|
|
5
|
+
import { effectUpdateFailuresNote } from './gimp-shared.js';
|
|
5
6
|
const TRANSFORM_LAYER_OPS = ['fit', 'scale', 'move', 'rotate', 'flip', 'skew', 'free'];
|
|
6
7
|
const xyProp = (what) => ({
|
|
7
8
|
type: 'object',
|
|
@@ -41,8 +42,8 @@ const transformLayerSchema = {
|
|
|
41
42
|
mode: {
|
|
42
43
|
type: 'string',
|
|
43
44
|
enum: ['fit', 'fill'],
|
|
44
|
-
description: "fit only. 'fit' letterboxes inside the canvas
|
|
45
|
-
'(
|
|
45
|
+
description: "fit only. 'fit' letterboxes the whole layer inside the canvas; 'fill' covers the canvas " +
|
|
46
|
+
'(overflow is left outside the canvas, not cropped). Defaults to ' +
|
|
46
47
|
"'fit'.",
|
|
47
48
|
},
|
|
48
49
|
scale_percent: {
|
|
@@ -4,7 +4,7 @@ import { basename } from 'node:path';
|
|
|
4
4
|
import { validateArgs } from '../utils/validate.js';
|
|
5
5
|
import { toolGimpErrorResult, unknownDiscriminator } from '../utils/tool-helpers.js';
|
|
6
6
|
import { Logger } from '../utils/logger.js';
|
|
7
|
-
import { GIMP_IMAGE_PROP, GIMP_REGION_PROP, GIMP_MAX_PX_PROP, pickSchemaDeclaredKeys, readRender, defaultPreviewsAllowed, } from './gimp-shared.js';
|
|
7
|
+
import { GIMP_IMAGE_PROP, GIMP_REGION_PROP, GIMP_MAX_PX_PROP, pickSchemaDeclaredKeys, readRender, defaultPreviewsAllowed, withWarning, } from './gimp-shared.js';
|
|
8
8
|
import { regionStatsLines } from './gimp-result-text.js';
|
|
9
9
|
const logger = new Logger('GimpVerifyTools');
|
|
10
10
|
const CHANNELS = ['luminance', 'red', 'green', 'blue'];
|
|
@@ -71,7 +71,10 @@ async function gimpGetPreview(gimp, rawArgs, previewsAllowedFn) {
|
|
|
71
71
|
: `privacy.send_previews_to_llm is false — image not returned to the model; ${fileNote}.`) +
|
|
72
72
|
unmirroredNote,
|
|
73
73
|
});
|
|
74
|
-
return {
|
|
74
|
+
return {
|
|
75
|
+
content,
|
|
76
|
+
structuredContent: withWarning({ ...result, path: published ? latestName : null }, unmirroredNote),
|
|
77
|
+
};
|
|
75
78
|
}
|
|
76
79
|
finally {
|
|
77
80
|
await rm(renderPath, { force: true }).catch(() => undefined);
|
|
@@ -132,7 +135,7 @@ async function gimpGetHistogram(gimp, rawArgs) {
|
|
|
132
135
|
text: `Histogram (${result.exact ? 'exact' : 'proxy'}${result.mask ? `, mask ${result.mask}` : ''}, ${result.pixels.toLocaleString()} px): ${summary}${unmirroredNote}`,
|
|
133
136
|
},
|
|
134
137
|
],
|
|
135
|
-
structuredContent: result,
|
|
138
|
+
structuredContent: withWarning(result, unmirroredNote),
|
|
136
139
|
};
|
|
137
140
|
}
|
|
138
141
|
catch (error) {
|
|
@@ -252,7 +255,10 @@ async function gimpCompare(gimp, rawArgs, previewsAllowedFn) {
|
|
|
252
255
|
: '') +
|
|
253
256
|
unmirroredNote,
|
|
254
257
|
});
|
|
255
|
-
return {
|
|
258
|
+
return {
|
|
259
|
+
content,
|
|
260
|
+
structuredContent: withWarning(reported, unmirroredNote),
|
|
261
|
+
};
|
|
256
262
|
}
|
|
257
263
|
finally {
|
|
258
264
|
if (beforePath)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateArgs } from '../utils/validate.js';
|
|
1
2
|
import { runSnippetTool } from '../utils/tool-helpers.js';
|
|
2
3
|
const addGuideSchema = {
|
|
3
4
|
type: 'object',
|
|
@@ -79,6 +80,7 @@ export function createGuideTools(connection, snippetClient) {
|
|
|
79
80
|
];
|
|
80
81
|
}
|
|
81
82
|
async function guides(connection, snippetClient, rawArgs) {
|
|
83
|
+
validateArgs({ type: 'object', properties: { op: { type: 'string' } }, required: ['op'] }, rawArgs);
|
|
82
84
|
const op = rawArgs.op;
|
|
83
85
|
const { op: _omit, ...rest } = rawArgs;
|
|
84
86
|
switch (op) {
|
|
@@ -39,7 +39,7 @@ export function createHistoryTools(connection, snippetClient) {
|
|
|
39
39
|
{
|
|
40
40
|
tool: {
|
|
41
41
|
name: 'ps_undo',
|
|
42
|
-
description: 'Step backward in the document history (equivalent to Ctrl/Cmd+Z). WHEN TO REACH FOR THIS: after an experimental destructive bake (ps_merge mode=visible/flatten, ps_filter op=apply type=*_blur/sharpen/noise) produced a wrong result;
|
|
42
|
+
description: 'Step backward in the document history (equivalent to Ctrl/Cmd+Z). WHEN TO REACH FOR THIS: after an experimental destructive bake (ps_merge mode=visible/flatten, ps_filter op=apply type=*_blur/sharpen/noise) produced a wrong result; or to revert an entire branch of exploration. Non-destructive workflows (adjustment layers + masks) rarely need this — just tweak/delete the offending layer instead. Reversible via ps_redo as long as no new edit has been made since. Returns current history state, remaining steps, and document context.',
|
|
43
43
|
inputSchema: undoSchema,
|
|
44
44
|
outputSchema: HISTORY_STEP_RESULT_SCHEMA,
|
|
45
45
|
annotations: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runSnippetTool } from '../utils/tool-helpers.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireLocalImagePath } from '../utils/local-path.js';
|
|
3
3
|
const PS_COORD_MAX_PX = 300_000;
|
|
4
4
|
const placeImageSchema = {
|
|
5
5
|
type: 'object',
|
|
@@ -74,7 +74,7 @@ async function placeImage(connection, snippetClient, rawArgs) {
|
|
|
74
74
|
errorPrefix: 'Error placing image',
|
|
75
75
|
params: (args) => {
|
|
76
76
|
const params = {
|
|
77
|
-
filePath:
|
|
77
|
+
filePath: requireLocalImagePath('file_path', args.file_path),
|
|
78
78
|
x: args.x,
|
|
79
79
|
y: args.y,
|
|
80
80
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateArgs } from '../utils/validate.js';
|
|
1
2
|
import { getMetadata, getMetadataSchema } from './metadata-tools.js';
|
|
2
3
|
import { getHistory } from './history-tools.js';
|
|
3
4
|
import { getLayerTree } from './layer-tools.js';
|
|
@@ -78,6 +79,7 @@ export function createInspectTools(connection, snippetClient) {
|
|
|
78
79
|
];
|
|
79
80
|
}
|
|
80
81
|
async function inspect(connection, snippetClient, rawArgs) {
|
|
82
|
+
validateArgs({ type: 'object', properties: { what: { type: 'string' } }, required: ['what'] }, rawArgs);
|
|
81
83
|
const what = rawArgs.what;
|
|
82
84
|
const { what: _omit, ...rest } = rawArgs;
|
|
83
85
|
switch (what) {
|
|
@@ -96,7 +98,7 @@ async function inspect(connection, snippetClient, rawArgs) {
|
|
|
96
98
|
content: [
|
|
97
99
|
{
|
|
98
100
|
type: 'text',
|
|
99
|
-
text: `Error: unknown
|
|
101
|
+
text: `Error: unknown \`what\` value "${String(what)}". Allowed: ${INSPECT_WHATS.join(', ')}.`,
|
|
100
102
|
},
|
|
101
103
|
],
|
|
102
104
|
isError: true,
|
package/dist/tools/path-tools.js
CHANGED
|
@@ -39,7 +39,7 @@ const pathInputSchema = {
|
|
|
39
39
|
},
|
|
40
40
|
placement: {
|
|
41
41
|
...PLACEMENT_SCHEMA,
|
|
42
|
-
description: 'create_from_placement only: NAME the curve the path follows, as an anchor-relational placement that resolves to a PATH (relation `along` a traced edge / a
|
|
42
|
+
description: 'create_from_placement only: NAME the curve the path follows, as an anchor-relational placement that resolves to a PATH (relation `along` a traced edge / a landmark curve, where available, or a `segment` between two anchors). The spatial-grounding resolver + objective gate produce AND verify the curve; the path is created ONLY if the gate PASSES. Name the curve — do NOT hand-type anchor points.',
|
|
43
43
|
},
|
|
44
44
|
closed: {
|
|
45
45
|
type: 'boolean',
|
|
@@ -119,7 +119,7 @@ export function createPortraitTools(connection, snippetClient, client = new Onnx
|
|
|
119
119
|
{
|
|
120
120
|
tool: {
|
|
121
121
|
name: 'ps_portrait_touchup',
|
|
122
|
-
description: 'Detection-driven face touch-ups using LOCAL on-device face detection. op=dodge_face brightens the detected face(s) with a non-destructive masked adjustment layer. op=soften_skin applies a soft blur over the face(s) on a duplicate layer (light, bounding-box-level smoothing). Works from the face bounding box — precise landmark ops (eyes/teeth/lips) are
|
|
122
|
+
description: 'Detection-driven face touch-ups using LOCAL on-device face detection. op=dodge_face brightens the detected face(s) with a non-destructive masked adjustment layer. op=soften_skin applies a soft blur over the face(s) on a duplicate layer (light, bounding-box-level smoothing). Works from the face bounding box — precise landmark ops (eyes/teeth/lips) are not part of this tool. Multiple faces are handled together. Pair with ps_detect (target=faces) to preview detection first.',
|
|
123
123
|
inputSchema: portraitSchema,
|
|
124
124
|
outputSchema: {
|
|
125
125
|
type: 'object',
|
|
@@ -224,7 +224,7 @@ export function createPreviewTools(connection, snippetClient) {
|
|
|
224
224
|
{
|
|
225
225
|
tool: {
|
|
226
226
|
name: 'ps_get_layer_bounds_diff',
|
|
227
|
-
description: 'Numeric verification: compute the per-edge pixel deltas between a layer\'s actual bounds and a target rectangle. Returns left/top/right/bottom deltas (actual − target), scale ratio (actual_size / target_size), centroid offset, and a one-word verdict ("aligned", "shifted right", "layer too small", etc.). Use this AFTER
|
|
227
|
+
description: 'Numeric verification: compute the per-edge pixel deltas between a layer\'s actual bounds and a target rectangle. Returns left/top/right/bottom deltas (actual − target), scale ratio (actual_size / target_size), centroid offset, and a one-word verdict ("aligned", "shifted right", "layer too small", etc.). Use this AFTER ps_place_image / ps_transform_layer to confirm the operation produced the intended result — far more reliable than eyeballing a preview for fine alignment. Read-only. Layers nested in groups are found by name recursively.',
|
|
228
228
|
inputSchema: boundsDiffSchema,
|
|
229
229
|
outputSchema: {
|
|
230
230
|
type: 'object',
|
|
@@ -332,6 +332,7 @@ async function selectByReference(connection, snippet, client, rawArgs, proRefine
|
|
|
332
332
|
method: 'precomputed_channel',
|
|
333
333
|
passed: true,
|
|
334
334
|
confidence: 1,
|
|
335
|
+
note: 'Loaded from a saved channel. If the image changed since, re-run with refresh:true, and verify with ps_get_selection_preview.',
|
|
335
336
|
reasons: ['loaded the saved scene:* channel by name'],
|
|
336
337
|
selection_info: loaded.selection_info,
|
|
337
338
|
detail: { from: 'precompute' },
|
|
@@ -393,7 +394,7 @@ export function createSceneTools(connection, snippetClient, opts = {}) {
|
|
|
393
394
|
{
|
|
394
395
|
tool: {
|
|
395
396
|
name: 'ps_read_scene',
|
|
396
|
-
description: 'The full scene model — run this before a spatially-targeted edit, not the cheaper ps_detect: detected subjects (with the main one flagged) and faces in document pixels, a coarse sky/ground region map, the horizon line (y + placement + confidence), tonal zones (shadow/midtone/highlight bands + coverage), composition geometry (which thirds cell the subject sits in, balance, headroom), and the menu of selectable named regions. The structured model is complete on its own — pass `annotate:true` for an annotated preview JPEG when you actually need to see it drawn. Built using LOCAL on-device vision + classical CV;
|
|
397
|
+
description: 'The full scene model — run this before a spatially-targeted edit, not the cheaper ps_detect: detected subjects (with the main one flagged) and faces in document pixels, a coarse sky/ground region map, the horizon line (y + placement + confidence), tonal zones (shadow/midtone/highlight bands + coverage), composition geometry (which thirds cell the subject sits in, balance, headroom), and the menu of selectable named regions. The structured model is complete on its own — pass `annotate:true` for an annotated preview JPEG when you actually need to see it drawn. Built using LOCAL on-device vision + classical CV; annotate:true returns a JPEG to your AI client. Select regions by name with ps_select_by_reference instead of guessing a rectangle. Read-only: renders a throwaway duplicate. Perception is cached per document state, so repeated reads are cheap.',
|
|
397
398
|
inputSchema: sceneSchema,
|
|
398
399
|
outputSchema: {
|
|
399
400
|
type: 'object',
|
|
@@ -447,7 +448,7 @@ export function createSceneTools(connection, snippetClient, opts = {}) {
|
|
|
447
448
|
{
|
|
448
449
|
tool: {
|
|
449
450
|
name: 'ps_select_by_reference',
|
|
450
|
-
description: 'Select a region by NAME instead of coordinates — the natural-mask alternative to a rectangle — with a CONFIDENCE GATE. target=sky/ground/foliage/subject/face/shadows/highlights/skin/above_horizon resolves through the right Photoshop-native method (threshold for sky, invert-sky−subjects for ground, luminance for shadows/highlights, skin-tone colour ∩ the subject box, the detected face/subject box) and is SCORED before it is offered: a clean region is left selected; an unconfident one is NOT selected and reported as honest absence (the city with no real sky gets no sky).
|
|
451
|
+
description: 'Select a region by NAME instead of coordinates — the natural-mask alternative to a rectangle — with a CONFIDENCE GATE. target=sky/ground/foliage/subject/face/shadows/highlights/skin/above_horizon resolves through the right Photoshop-native method (threshold for sky, invert-sky−subjects for ground, luminance for shadows/highlights, skin-tone colour ∩ the subject box, the detected face/subject box) and is SCORED before it is offered: a clean region is left selected; an unconfident one is NOT selected and reported as honest absence (the city with no real sky gets no sky). Some builds add precise FACE-FEATURE targets backed by the face mesh — face_skin (the retouch mask: face minus eyes/brows/lips), face_eyes, face_brows, face_lips, face_teeth (mouth opening), face_nose, face_under_eye, face_cheeks — each a real geometry-following selection, derived on first request and then saved as a scene:face_* channel so repeats load instantly. `passed`/`confidence` are returned. A region derived here is cached as a `scene:*` channel keyed by TARGET ONLY, so a later call for the same target loads it by name; pass `refresh:true` to force a fresh derive after an edit that changes what the region means, and note that narrowing a call with `label`/`instance`/`composition_context` always derives (it neither reads nor writes that shared channel). The structural floor (coherence, horizon alignment) is never tuned; for an artistic/non-standard shot pass `composition_context` (e.g. profile:big_sky) to relax the compositional priors so a legitimately large sky is not rejected. For target=subject with several present, pass `label` and/or `instance`. Build/inspect with ps_read_scene first; verify with ps_get_selection_preview (the red-overlay is the human/agent oversight view). Prefer this over a rectangle for any real-world region.',
|
|
451
452
|
inputSchema: selectByReferenceSchema,
|
|
452
453
|
outputSchema: {
|
|
453
454
|
type: 'object',
|
|
@@ -398,7 +398,7 @@ const selectionPreviewSchema = {
|
|
|
398
398
|
type: 'string',
|
|
399
399
|
enum: ['overlay', 'mask', 'both'],
|
|
400
400
|
default: 'overlay',
|
|
401
|
-
description: "Which rendered image(s) to return inline. 'overlay' (default): a 50% red wash over the selected area (
|
|
401
|
+
description: "Which rendered image(s) to return inline. 'overlay' (default): a 50% red wash over the selected area (most intuitive). 'mask': a B/W mask (black=selected, white=not) instead. 'both': the overlay followed by the mask, for when you need to compare them side by side. selection_info is returned regardless of this choice.",
|
|
402
402
|
},
|
|
403
403
|
},
|
|
404
404
|
};
|
|
@@ -705,7 +705,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
705
705
|
{
|
|
706
706
|
tool: {
|
|
707
707
|
name: 'ps_get_selection_preview',
|
|
708
|
-
description: "Render an inline JPEG so the agent can visually verify what is currently selected: by default a red-wash OVERLAY (50% red over the selected area,
|
|
708
|
+
description: "Render an inline JPEG so the agent can visually verify what is currently selected: by default a red-wash OVERLAY (50% red over the selected area, most intuitive); pass `image:'mask'` for a B/W MASK (black = selected, white = not) instead, or `image:'both'` for both. selection_info is always returned regardless of `image`. Heavier than the selection_info bundle alone (~2-4s) — call this when the stats look off or before committing a mask. Does NOT modify the source document.",
|
|
709
709
|
inputSchema: selectionPreviewSchema,
|
|
710
710
|
outputSchema: {
|
|
711
711
|
type: 'object',
|
|
@@ -728,7 +728,7 @@ export function createSelectionTools(connection, snippetClient, client = new Onn
|
|
|
728
728
|
{
|
|
729
729
|
tool: {
|
|
730
730
|
name: 'ps_layer_mask',
|
|
731
|
-
description: 'Manage the active layer\'s mask — choose with `op`. `create`: add a layer mask; if a selection is active it reveals the selection and hides the rest (the one-call "mask the placed image to the frame opening" — make the selection first, then create); with no selection, a reveal-all mask; adjustment layers load the current selection into their built-in mask. `delete`: remove the mask (layer pixels preserved). `apply`: DESTRUCTIVE — permanently bake the mask into the pixels (outside-mask pixels lost). `gradient`: draw a linear fade into the mask (fade_to names the side that ends hidden; start/end place the fade; auto-creates the mask; replaces existing mask content and clears any active selection first) — the standard fade for reflections, sky blends, and soft edge falloff. Verify with ps_get_preview. Prefer keeping masks non-destructive unless flattening for export. **
|
|
731
|
+
description: 'Manage the active layer\'s mask — choose with `op`. `create`: add a layer mask; if a selection is active it reveals the selection and hides the rest (the one-call "mask the placed image to the frame opening" — make the selection first, then create); with no selection, a reveal-all mask; adjustment layers load the current selection into their built-in mask. `delete`: remove the mask (layer pixels preserved). `apply`: DESTRUCTIVE — permanently bake the mask into the pixels (outside-mask pixels lost). `gradient`: draw a linear fade into the mask (fade_to names the side that ends hidden; start/end place the fade; auto-creates the mask; replaces existing mask content and clears any active selection first) — the standard fade for reflections, sky blends, and soft edge falloff. Verify with ps_get_preview. Prefer keeping masks non-destructive unless flattening for export. **Prefer this tool over a hand-written mask script; it already handles masks and gradients.**',
|
|
732
732
|
inputSchema: LAYER_MASK_INPUT_SCHEMA,
|
|
733
733
|
outputSchema: {
|
|
734
734
|
type: 'object',
|
|
@@ -28,7 +28,7 @@ const shapeInputSchema = {
|
|
|
28
28
|
...PLACEMENT_SCHEMA,
|
|
29
29
|
description: 'ANCHOR-RELATIONAL placement (preferred over guessing pixels): rectangle/ellipse ← a region relation ' +
|
|
30
30
|
'(inside/gap) → the resolved region bounding box; line ← a path relation (along/offset-curve over a traced ' +
|
|
31
|
-
'edge or a
|
|
31
|
+
'edge or a face-mesh landmark curve, where available) → a straight line between the resolved curve endpoints. The shape ' +
|
|
32
32
|
'is created ONLY if the gate PASSES (otherwise an error and no layer). When set, left/top/right/bottom and ' +
|
|
33
33
|
'start_x/start_y/end_x/end_y are ignored, but styling (fill_color/stroke/weight/corner_radius) still ' +
|
|
34
34
|
'applies. See the placement-resolver tool, when this build has one, for the anchors + relation vocabulary.',
|
package/dist/tools/sky-tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { runSnippetTool } from '../utils/tool-helpers.js';
|
|
2
|
-
import {
|
|
2
|
+
import { requireLocalImagePath } from '../utils/local-path.js';
|
|
3
3
|
import { SKY_REPLACEMENT_TIMEOUT_MS } from '../utils/operation-timeouts.js';
|
|
4
4
|
import { defaultSkyPreset, loadSkyPresets, matchSkyPreset, } from '../utils/sky-presets.js';
|
|
5
5
|
const PLACEHOLDER_SKY_ID = '00000000-0000-0000-0000-000000000000';
|
|
@@ -73,7 +73,7 @@ function resolveSky(args, presets) {
|
|
|
73
73
|
if (args.sky_file !== undefined) {
|
|
74
74
|
return {
|
|
75
75
|
kind: 'file',
|
|
76
|
-
path:
|
|
76
|
+
path: requireLocalImagePath('sky_file', args.sky_file),
|
|
77
77
|
label: 'Custom Sky',
|
|
78
78
|
};
|
|
79
79
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { validateArgs } from '../utils/validate.js';
|
|
2
2
|
import { runSnippetTool, toolErrorResult } from '../utils/tool-helpers.js';
|
|
3
3
|
import { LAYER_BLEND_MODES } from '../utils/blend-modes.js';
|
|
4
|
+
const INDICES_RENUMBERED = 'Indices renumbered — re-run op=list before the next index-taking call.';
|
|
4
5
|
const SMART_FILTER_OPS = ['list', 'set_visibility', 'set_blend', 'remove'];
|
|
5
6
|
const smartFilterInputSchema = {
|
|
6
7
|
type: 'object',
|
|
@@ -116,9 +117,14 @@ export async function runSmartFilterOp(connection, snippetClient, rawArgs) {
|
|
|
116
117
|
params: (a) => ({ index: a.index }),
|
|
117
118
|
successText: (result) => {
|
|
118
119
|
const r = result;
|
|
119
|
-
return `Removed Smart Filter ${String(r.index)} (${String(r.removed_filter_name)}) from "${String(r.layer_name)}". ${String(r.remaining_count)} remaining.
|
|
120
|
+
return `Removed Smart Filter ${String(r.index)} (${String(r.removed_filter_name)}) from "${String(r.layer_name)}". ${String(r.remaining_count)} remaining. ${INDICES_RENUMBERED}`;
|
|
120
121
|
},
|
|
121
|
-
})
|
|
122
|
+
}).then((result) => result.isError
|
|
123
|
+
? result
|
|
124
|
+
: {
|
|
125
|
+
...result,
|
|
126
|
+
structuredContent: { ...result.structuredContent, note: INDICES_RENUMBERED },
|
|
127
|
+
});
|
|
122
128
|
default:
|
|
123
129
|
return toolErrorResult('Error in ps_filter', new Error(`Unknown smart-filter op: ${String(op)}. Allowed: ${SMART_FILTER_OPS.join(', ')}.`));
|
|
124
130
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateArgs } from '../utils/validate.js';
|
|
1
2
|
import { runSnippetTool } from '../utils/tool-helpers.js';
|
|
2
3
|
const rotateCanvasSchema = {
|
|
3
4
|
type: 'object',
|
|
@@ -60,6 +61,7 @@ export function createTransformCanvasTools(connection, snippetClient) {
|
|
|
60
61
|
];
|
|
61
62
|
}
|
|
62
63
|
async function transformCanvas(connection, snippetClient, rawArgs) {
|
|
64
|
+
validateArgs({ type: 'object', properties: { op: { type: 'string' } }, required: ['op'] }, rawArgs);
|
|
63
65
|
const op = rawArgs.op;
|
|
64
66
|
const { op: _omit, ...rest } = rawArgs;
|
|
65
67
|
switch (op) {
|
package/dist/utils/local-path.js
CHANGED
|
@@ -53,6 +53,21 @@ export function requireLocalPath(field, value, opts = {}) {
|
|
|
53
53
|
throw new PathArgumentError(`"${field}" does not resolve to a stable absolute path ("${String(value)}"). Pass a plain, full path.`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
function withoutTrailingDotsAndSpaces(path) {
|
|
57
|
+
let end = path.length;
|
|
58
|
+
while (end > 0 && (path[end - 1] === '.' || path[end - 1] === ' '))
|
|
59
|
+
end--;
|
|
60
|
+
return path.slice(0, end);
|
|
61
|
+
}
|
|
62
|
+
const NOT_AN_IMAGE_EXT_RE = /\.(jsx|jsxbin|jsxinc|js|psjs|atn|abr|pat|asl|grd|tpl)$/i;
|
|
63
|
+
export function requireLocalImagePath(field, value, opts = {}) {
|
|
64
|
+
const path = requireLocalPath(field, value, opts);
|
|
65
|
+
if (NOT_AN_IMAGE_EXT_RE.test(withoutTrailingDotsAndSpaces(path))) {
|
|
66
|
+
throw new PathArgumentError(`"${field}" is a script, Action or preset file, not an image: "${String(value)}". ` +
|
|
67
|
+
'Open or place an image or document instead.');
|
|
68
|
+
}
|
|
69
|
+
return path;
|
|
70
|
+
}
|
|
56
71
|
const PERCENT_ESCAPE_RE = /%[0-9A-Fa-f]{2}/;
|
|
57
72
|
const WIN32_DEVICE_NAME_RE = /^(con|prn|aux|nul|conin\$|conout\$|com[0-9¹²³]|lpt[0-9¹²³])\s*(\..*)?$/i;
|
|
58
73
|
function refuseExtendScriptAmbiguity(field, path, platform) {
|
package/dist/utils/validate.js
CHANGED
|
@@ -4,6 +4,10 @@ export class ValidationError extends Error {
|
|
|
4
4
|
this.name = 'ValidationError';
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
export function missingRequiredArg(schema, args) {
|
|
8
|
+
const props = (schema.properties ?? {});
|
|
9
|
+
return schema.required?.find((key) => (args[key] === undefined || args[key] === null) && props[key]?.default === undefined);
|
|
10
|
+
}
|
|
7
11
|
export function validateArgs(schema, args) {
|
|
8
12
|
const input = args ?? {};
|
|
9
13
|
const out = {};
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.9.
|
|
1
|
+
export const VERSION = '1.9.5';
|
package/package.json
CHANGED