automify 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +401 -0
- package/SECURITY.md +17 -0
- package/examples/anthropic-provider.js +18 -0
- package/examples/browser-basic.js +30 -0
- package/examples/browser-with-safety.js +38 -0
- package/examples/claude-model-adapter.js +141 -0
- package/examples/cli-basic.js +20 -0
- package/examples/cli-docker.js +42 -0
- package/examples/custom-computer.js +18 -0
- package/examples/custom-model-adapter.js +48 -0
- package/examples/desktop-docker.js +37 -0
- package/examples/desktop-local.js +28 -0
- package/examples/evaluate-image.js +26 -0
- package/examples/files-and-shared-folder.js +42 -0
- package/package.json +74 -0
- package/scripts/generate-argument-reference.js +17 -0
- package/scripts/install-browser.js +12 -0
- package/scripts/install-desktop.js +281 -0
- package/src/index.d.ts +1049 -0
- package/src/index.js +83 -0
- package/src/lib/adapter-locks.js +93 -0
- package/src/lib/adapter-toolkit.js +239 -0
- package/src/lib/anthropic-model-adapter.js +451 -0
- package/src/lib/argument-reference.js +98 -0
- package/src/lib/automify.js +938 -0
- package/src/lib/browser-automify.js +89 -0
- package/src/lib/cli-automify.js +520 -0
- package/src/lib/computer-automify.js +103 -0
- package/src/lib/docker-cli-automify.js +517 -0
- package/src/lib/docker-desktop-computer.js +725 -0
- package/src/lib/errors.js +24 -0
- package/src/lib/file-data.js +140 -0
- package/src/lib/init.js +217 -0
- package/src/lib/local-desktop-computer.js +963 -0
- package/src/lib/model-adapter.js +32 -0
- package/src/lib/openai-responses-client.js +162 -0
- package/src/lib/output.js +57 -0
- package/src/lib/playwright-computer.js +363 -0
- package/src/lib/presets.js +141 -0
- package/src/lib/result.js +95 -0
- package/src/lib/runtime.js +471 -0
- package/src/lib/virtual-shared-folder.js +109 -0
- package/src/lib/zod-output.js +26 -0
- package/src/zod.d.ts +12 -0
- package/src/zod.js +5 -0
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
|
|
8
|
+
import { AutomifyError } from "./errors.js";
|
|
9
|
+
import { acquireAdapterLock } from "./adapter-locks.js";
|
|
10
|
+
import { assertKnownOptions, normalizeLogFile, writeDebugLogFile } from "./runtime.js";
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile);
|
|
13
|
+
const LOCAL_DESKTOP_OPTION_KEYS = new Set([
|
|
14
|
+
"nut",
|
|
15
|
+
"viewport",
|
|
16
|
+
"display",
|
|
17
|
+
"displayWidth",
|
|
18
|
+
"displayHeight",
|
|
19
|
+
"environment",
|
|
20
|
+
"waitMs",
|
|
21
|
+
"actionDelayMs",
|
|
22
|
+
"instructions",
|
|
23
|
+
"screenshotPath",
|
|
24
|
+
"pixelScale",
|
|
25
|
+
"mouseScaleX",
|
|
26
|
+
"mouseScaleY",
|
|
27
|
+
"mouseOffsetX",
|
|
28
|
+
"mouseOffsetY",
|
|
29
|
+
"mouseAutoDelayMs",
|
|
30
|
+
"keyboardAutoDelayMs",
|
|
31
|
+
"mouse",
|
|
32
|
+
"keyboard",
|
|
33
|
+
"calibration",
|
|
34
|
+
"virtualDisplay",
|
|
35
|
+
"forceVirtualDisplay",
|
|
36
|
+
"virtualDisplayDisplay",
|
|
37
|
+
"virtualDisplayWidth",
|
|
38
|
+
"virtualDisplayHeight",
|
|
39
|
+
"virtualDisplayDepth",
|
|
40
|
+
"virtualDisplayCommand",
|
|
41
|
+
"virtualDisplayArgs",
|
|
42
|
+
"virtualDisplayStartupMs",
|
|
43
|
+
"mouseSpeed",
|
|
44
|
+
"smoothMouseMove",
|
|
45
|
+
"configureMouse",
|
|
46
|
+
"configureKeyboard",
|
|
47
|
+
"macCommandTabHoldMs",
|
|
48
|
+
"macCommandTabSettleMs",
|
|
49
|
+
"silent",
|
|
50
|
+
"debug",
|
|
51
|
+
"logFile",
|
|
52
|
+
"macosDisplayInfo",
|
|
53
|
+
"calibrateScreenshot",
|
|
54
|
+
"requireCalibration",
|
|
55
|
+
"screenshot",
|
|
56
|
+
"onUnknownAction",
|
|
57
|
+
"context",
|
|
58
|
+
"coordinateSpace",
|
|
59
|
+
"actionType",
|
|
60
|
+
"env",
|
|
61
|
+
"spawn"
|
|
62
|
+
]);
|
|
63
|
+
export const LOCAL_DESKTOP_COMPUTER_OPTION_KEYS = LOCAL_DESKTOP_OPTION_KEYS;
|
|
64
|
+
const LOCAL_DISPLAY_KEYS = new Set(["width", "height", "pixelScale"]);
|
|
65
|
+
const LOCAL_MOUSE_KEYS = new Set([
|
|
66
|
+
"scaleX",
|
|
67
|
+
"scaleY",
|
|
68
|
+
"offsetX",
|
|
69
|
+
"offsetY",
|
|
70
|
+
"autoDelayMs",
|
|
71
|
+
"speed",
|
|
72
|
+
"smooth",
|
|
73
|
+
"configure"
|
|
74
|
+
]);
|
|
75
|
+
const LOCAL_KEYBOARD_KEYS = new Set(["autoDelayMs", "configure"]);
|
|
76
|
+
const LOCAL_CALIBRATION_KEYS = new Set([
|
|
77
|
+
"pixelScale",
|
|
78
|
+
"mouseScaleX",
|
|
79
|
+
"mouseScaleY",
|
|
80
|
+
"mouseOffsetX",
|
|
81
|
+
"mouseOffsetY",
|
|
82
|
+
"screenshot",
|
|
83
|
+
"required"
|
|
84
|
+
]);
|
|
85
|
+
const LOCAL_VIRTUAL_DISPLAY_KEYS = new Set(["display", "width", "height", "depth", "command", "args", "startupMs"]);
|
|
86
|
+
|
|
87
|
+
const KEY_ALIASES = new Map([
|
|
88
|
+
["alt", "LeftAlt"],
|
|
89
|
+
["option", "LeftAlt"],
|
|
90
|
+
["backspace", "Backspace"],
|
|
91
|
+
["cmd", "LeftCmd"],
|
|
92
|
+
["command", "LeftCmd"],
|
|
93
|
+
["control", "LeftControl"],
|
|
94
|
+
["ctrl", "LeftControl"],
|
|
95
|
+
["delete", "Delete"],
|
|
96
|
+
["down", "Down"],
|
|
97
|
+
["end", "End"],
|
|
98
|
+
["enter", "Enter"],
|
|
99
|
+
["esc", "Escape"],
|
|
100
|
+
["escape", "Escape"],
|
|
101
|
+
["home", "Home"],
|
|
102
|
+
["left", "Left"],
|
|
103
|
+
["meta", "LeftCmd"],
|
|
104
|
+
["page_down", "PageDown"],
|
|
105
|
+
["pagedown", "PageDown"],
|
|
106
|
+
["page_up", "PageUp"],
|
|
107
|
+
["pageup", "PageUp"],
|
|
108
|
+
["return", "Enter"],
|
|
109
|
+
["right", "Right"],
|
|
110
|
+
["shift", "LeftShift"],
|
|
111
|
+
["space", "Space"],
|
|
112
|
+
["tab", "Tab"],
|
|
113
|
+
["up", "Up"]
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const DEFAULT_DESKTOP_ACTION_DELAY_MS = 250;
|
|
117
|
+
const DEFAULT_DESKTOP_INSTRUCTIONS = [
|
|
118
|
+
"You are controlling a native desktop through screenshots and mouse/keyboard actions.",
|
|
119
|
+
"Orient from the screenshot first: identify the active app, visible window, focused field, current page, and the specific target required by the task before acting.",
|
|
120
|
+
"Use deterministic entry points. For a website or web app, use the browser address bar only when a browser is clearly focused; otherwise open the browser through a visible Dock/app icon or OS search/launcher, then use the address bar. For app content, use visible in-app search, filters, or navigation controls.",
|
|
121
|
+
"Do not open or use Command+Tab, Alt+Tab, Mission Control, or other cyclic app/window switchers unless the task explicitly asks to switch to the previous app. Cyclic switching is unreliable because the open-app order is unknown.",
|
|
122
|
+
"Do not click as a probe. Click only when the screenshot shows a specific visible target and the purpose of that click is clear from the task or current UI. Prefer named controls, fields, menu items, visible app icons, and address/search fields over unlabeled areas.",
|
|
123
|
+
"If the target is not visible, choose a deterministic recovery path: direct URL, OS search/launcher, in-app search, visible navigation, or a screenshot/wait when loading is visible. Do not repeat nearly identical clicks after no visible change.",
|
|
124
|
+
"After any action that opens an app, navigates, submits input, changes windows, or might trigger loading, use the next screenshot to decide the next step. Stop when the requested result is known; do not keep interacting to confirm unnecessarily."
|
|
125
|
+
].join("\n");
|
|
126
|
+
|
|
127
|
+
export async function createLocalDesktopComputer(options = {}) {
|
|
128
|
+
options = normalizeLocalDesktopOptions(options);
|
|
129
|
+
const releaseLock = await acquireAdapterLock("local-desktop", {
|
|
130
|
+
label: "local desktop adapter"
|
|
131
|
+
});
|
|
132
|
+
const setupStartedAt = Date.now();
|
|
133
|
+
debugLocalDesktop(options, "setup_start");
|
|
134
|
+
let virtualDisplay;
|
|
135
|
+
try {
|
|
136
|
+
virtualDisplay = await ensureLinuxVirtualDisplay(options);
|
|
137
|
+
const nut = options.nut ?? (await importNut());
|
|
138
|
+
configureNutMouse(nut, options);
|
|
139
|
+
configureNutKeyboard(nut, options);
|
|
140
|
+
const environment = options.environment ?? defaultDesktopEnvironment();
|
|
141
|
+
const screenWidth = await maybeCall(nut.screen?.width, nut.screen);
|
|
142
|
+
const screenHeight = await maybeCall(nut.screen?.height, nut.screen);
|
|
143
|
+
const calibration = await calibrateLocalDesktop(nut, options, {
|
|
144
|
+
screenWidth,
|
|
145
|
+
screenHeight
|
|
146
|
+
});
|
|
147
|
+
const macOSDisplay = await getMacOSDisplayInfo(options, environment);
|
|
148
|
+
let initialScreenshot = calibration.screenshot;
|
|
149
|
+
const displayWidth = options.displayWidth ?? calibration.width ?? screenWidth;
|
|
150
|
+
const displayHeight = options.displayHeight ?? calibration.height ?? screenHeight;
|
|
151
|
+
const coordinateSpace = buildCoordinateSpace(options, {
|
|
152
|
+
displayWidth,
|
|
153
|
+
displayHeight,
|
|
154
|
+
screenWidth,
|
|
155
|
+
screenHeight,
|
|
156
|
+
environment,
|
|
157
|
+
macOSDisplay
|
|
158
|
+
});
|
|
159
|
+
debugLocalDesktop(options, "coordinate_space", summarizeCoordinateSpace(coordinateSpace));
|
|
160
|
+
debugLocalDesktop(options, "setup_complete", {
|
|
161
|
+
displayWidth,
|
|
162
|
+
displayHeight,
|
|
163
|
+
environment,
|
|
164
|
+
screenshotBytes: calibration.screenshot?.byteLength,
|
|
165
|
+
durationMs: Date.now() - setupStartedAt
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
displayWidth,
|
|
170
|
+
displayHeight,
|
|
171
|
+
environment,
|
|
172
|
+
instructions: options.instructions ?? DEFAULT_DESKTOP_INSTRUCTIONS,
|
|
173
|
+
|
|
174
|
+
async execute(action, context) {
|
|
175
|
+
await executeLocalDesktopAction(action, {
|
|
176
|
+
...options,
|
|
177
|
+
actionDelayMs: options.actionDelayMs ?? DEFAULT_DESKTOP_ACTION_DELAY_MS,
|
|
178
|
+
context,
|
|
179
|
+
debug: options.debug,
|
|
180
|
+
coordinateSpace,
|
|
181
|
+
nut
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async screenshot(context) {
|
|
186
|
+
if (typeof options.screenshot === "function") {
|
|
187
|
+
return options.screenshot(context);
|
|
188
|
+
}
|
|
189
|
+
if (context?.initial && initialScreenshot) {
|
|
190
|
+
const screenshot = initialScreenshot;
|
|
191
|
+
initialScreenshot = null;
|
|
192
|
+
return screenshot;
|
|
193
|
+
}
|
|
194
|
+
return captureLocalDesktopScreenshot({
|
|
195
|
+
...options,
|
|
196
|
+
context,
|
|
197
|
+
nut
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async close() {
|
|
202
|
+
try {
|
|
203
|
+
await virtualDisplay?.close();
|
|
204
|
+
} finally {
|
|
205
|
+
await releaseLock?.();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
} catch (error) {
|
|
210
|
+
await virtualDisplay?.close();
|
|
211
|
+
await releaseLock?.();
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function executeLocalDesktopAction(action, options = {}) {
|
|
217
|
+
options = normalizeLocalDesktopOptions(options);
|
|
218
|
+
const nut = options.nut ?? (await importNut());
|
|
219
|
+
configureNutMouse(nut, options);
|
|
220
|
+
configureNutKeyboard(nut, options);
|
|
221
|
+
debugLocalDesktop(options, "action", {
|
|
222
|
+
action,
|
|
223
|
+
coordinateSpace: summarizeCoordinateSpace(options.coordinateSpace)
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (!action || typeof action !== "object") {
|
|
227
|
+
throw new AutomifyError("local desktop action must be an object.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
switch (action.type) {
|
|
231
|
+
case "click":
|
|
232
|
+
await moveMouse(nut, action.x, action.y, { ...options, actionType: "click" });
|
|
233
|
+
await nut.mouse.click(nutButton(nut, action.button));
|
|
234
|
+
await settleAfterAction(options);
|
|
235
|
+
break;
|
|
236
|
+
case "double_click":
|
|
237
|
+
await moveMouse(nut, action.x, action.y, { ...options, actionType: "click" });
|
|
238
|
+
await nut.mouse.doubleClick(nutButton(nut, action.button));
|
|
239
|
+
await settleAfterAction(options);
|
|
240
|
+
break;
|
|
241
|
+
case "scroll":
|
|
242
|
+
await moveMouse(nut, action.x, action.y, options);
|
|
243
|
+
await scrollMouse(nut, action);
|
|
244
|
+
await settleAfterAction(options);
|
|
245
|
+
break;
|
|
246
|
+
case "keypress":
|
|
247
|
+
await pressKeys(nut, action.keys ?? [action.key].filter(Boolean), options);
|
|
248
|
+
await settleAfterAction(options);
|
|
249
|
+
break;
|
|
250
|
+
case "type":
|
|
251
|
+
await nut.keyboard.type(String(action.text ?? ""));
|
|
252
|
+
await settleAfterAction(options);
|
|
253
|
+
break;
|
|
254
|
+
case "wait":
|
|
255
|
+
await delay(options.waitMs ?? action.ms ?? action.duration_ms ?? 1000);
|
|
256
|
+
break;
|
|
257
|
+
case "screenshot":
|
|
258
|
+
break;
|
|
259
|
+
case "move":
|
|
260
|
+
await moveMouse(nut, action.x, action.y, options);
|
|
261
|
+
break;
|
|
262
|
+
case "drag":
|
|
263
|
+
await dragMouse(nut, action, options);
|
|
264
|
+
await settleAfterAction(options);
|
|
265
|
+
break;
|
|
266
|
+
default:
|
|
267
|
+
if (typeof options.onUnknownAction === "function") {
|
|
268
|
+
await options.onUnknownAction(action, options.context);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
throw new AutomifyError(`Unsupported local desktop action: ${action.type}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function settleAfterAction(options = {}) {
|
|
276
|
+
const waitMs = Math.max(0, Number(options.actionDelayMs) || 0);
|
|
277
|
+
if (waitMs > 0) {
|
|
278
|
+
await delay(waitMs);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function captureLocalDesktopScreenshot(options = {}) {
|
|
283
|
+
options = normalizeLocalDesktopOptions(options);
|
|
284
|
+
const nut = options.nut ?? (await importNut());
|
|
285
|
+
const startedAt = Date.now();
|
|
286
|
+
const result = await captureLocalDesktopScreenshotToFile(nut, options);
|
|
287
|
+
|
|
288
|
+
if (!isImageObject(result)) {
|
|
289
|
+
debugLocalDesktop(options, "screenshot_capture", {
|
|
290
|
+
bytes: result?.byteLength,
|
|
291
|
+
durationMs: Date.now() - startedAt
|
|
292
|
+
});
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
if (typeof nut.saveImage !== "function") {
|
|
296
|
+
throw new AutomifyError(
|
|
297
|
+
"local desktop screenshot capture returned an image object, but saveImage() is unavailable."
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const bytes = await saveNutImageObject(nut, result, options);
|
|
302
|
+
debugLocalDesktop(options, "screenshot_capture", {
|
|
303
|
+
bytes: bytes?.byteLength,
|
|
304
|
+
durationMs: Date.now() - startedAt
|
|
305
|
+
});
|
|
306
|
+
return bytes;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function normalizeLocalDesktopOptions(options = {}) {
|
|
310
|
+
assertKnownOptions("local desktop adapter", options, LOCAL_DESKTOP_OPTION_KEYS);
|
|
311
|
+
assertKnownOptions(
|
|
312
|
+
"local desktop display",
|
|
313
|
+
typeof options.display === "object" ? options.display : null,
|
|
314
|
+
LOCAL_DISPLAY_KEYS
|
|
315
|
+
);
|
|
316
|
+
assertKnownOptions("local desktop mouse", options.mouse, LOCAL_MOUSE_KEYS);
|
|
317
|
+
assertKnownOptions("local desktop keyboard", options.keyboard, LOCAL_KEYBOARD_KEYS);
|
|
318
|
+
assertKnownOptions("local desktop calibration", options.calibration, LOCAL_CALIBRATION_KEYS);
|
|
319
|
+
assertKnownOptions(
|
|
320
|
+
"local desktop virtualDisplay",
|
|
321
|
+
typeof options.virtualDisplay === "object" ? options.virtualDisplay : null,
|
|
322
|
+
LOCAL_VIRTUAL_DISPLAY_KEYS
|
|
323
|
+
);
|
|
324
|
+
const viewport = options.viewport ?? {};
|
|
325
|
+
const display = typeof options.display === "object" ? options.display : {};
|
|
326
|
+
const mouse = options.mouse ?? {};
|
|
327
|
+
const keyboard = options.keyboard ?? {};
|
|
328
|
+
const calibration = options.calibration ?? {};
|
|
329
|
+
const virtualDisplay = typeof options.virtualDisplay === "object" ? options.virtualDisplay : {};
|
|
330
|
+
return {
|
|
331
|
+
...options,
|
|
332
|
+
debug: options.debug ?? false,
|
|
333
|
+
logFile: normalizeLogFile(options.logFile, "local desktop adapter logFile"),
|
|
334
|
+
display: typeof options.display === "object" ? undefined : options.display,
|
|
335
|
+
displayWidth: options.displayWidth ?? viewport.width ?? display.width,
|
|
336
|
+
displayHeight: options.displayHeight ?? viewport.height ?? display.height,
|
|
337
|
+
pixelScale: options.pixelScale ?? display.pixelScale ?? calibration.pixelScale,
|
|
338
|
+
mouseScaleX: options.mouseScaleX ?? mouse.scaleX ?? calibration.mouseScaleX,
|
|
339
|
+
mouseScaleY: options.mouseScaleY ?? mouse.scaleY ?? calibration.mouseScaleY,
|
|
340
|
+
mouseOffsetX: options.mouseOffsetX ?? mouse.offsetX ?? calibration.mouseOffsetX,
|
|
341
|
+
mouseOffsetY: options.mouseOffsetY ?? mouse.offsetY ?? calibration.mouseOffsetY,
|
|
342
|
+
mouseAutoDelayMs: options.mouseAutoDelayMs ?? mouse.autoDelayMs,
|
|
343
|
+
mouseSpeed: options.mouseSpeed ?? mouse.speed,
|
|
344
|
+
smoothMouseMove: options.smoothMouseMove ?? mouse.smooth,
|
|
345
|
+
configureMouse: options.configureMouse ?? mouse.configure,
|
|
346
|
+
keyboardAutoDelayMs: options.keyboardAutoDelayMs ?? keyboard.autoDelayMs,
|
|
347
|
+
configureKeyboard: options.configureKeyboard ?? keyboard.configure,
|
|
348
|
+
virtualDisplay: typeof options.virtualDisplay === "object" ? true : options.virtualDisplay,
|
|
349
|
+
virtualDisplayDisplay: options.virtualDisplayDisplay ?? virtualDisplay.display,
|
|
350
|
+
virtualDisplayWidth: options.virtualDisplayWidth ?? virtualDisplay.width,
|
|
351
|
+
virtualDisplayHeight: options.virtualDisplayHeight ?? virtualDisplay.height,
|
|
352
|
+
virtualDisplayDepth: options.virtualDisplayDepth ?? virtualDisplay.depth,
|
|
353
|
+
virtualDisplayCommand: options.virtualDisplayCommand ?? virtualDisplay.command,
|
|
354
|
+
virtualDisplayArgs: options.virtualDisplayArgs ?? virtualDisplay.args,
|
|
355
|
+
virtualDisplayStartupMs: options.virtualDisplayStartupMs ?? virtualDisplay.startupMs,
|
|
356
|
+
calibrateScreenshot: options.calibrateScreenshot ?? calibration.screenshot,
|
|
357
|
+
requireCalibration: options.requireCalibration ?? calibration.required
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function captureLocalDesktopScreenshotToFile(nut, options = {}) {
|
|
362
|
+
if (typeof nut.screen?.capture !== "function") {
|
|
363
|
+
throw new AutomifyError("local desktop screen.capture() is unavailable.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const filename = `automify-nut-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
367
|
+
const directory = options.screenshotPath ? undefined : tmpdir();
|
|
368
|
+
const requestedPath = options.screenshotPath;
|
|
369
|
+
const capturedPath = await nut.screen.capture(requestedPath ?? filename, nut.FileType?.PNG, directory);
|
|
370
|
+
|
|
371
|
+
if (isByteLike(capturedPath)) return capturedPath;
|
|
372
|
+
if (isImageObject(capturedPath)) return capturedPath;
|
|
373
|
+
|
|
374
|
+
const path = capturedPath ?? requestedPath ?? join(directory, `${filename}.png`);
|
|
375
|
+
|
|
376
|
+
try {
|
|
377
|
+
return await readFile(path);
|
|
378
|
+
} finally {
|
|
379
|
+
if (!options.screenshotPath) {
|
|
380
|
+
await unlink(path).catch(() => {});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function calibrateLocalDesktop(nut, options, screen) {
|
|
386
|
+
if (options.calibrateScreenshot === false || typeof options.screenshot === "function") {
|
|
387
|
+
return {};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const screenshot = await captureLocalDesktopScreenshot({
|
|
392
|
+
...options,
|
|
393
|
+
nut
|
|
394
|
+
});
|
|
395
|
+
const dimensions = pngDimensions(screenshot);
|
|
396
|
+
return {
|
|
397
|
+
screenshot,
|
|
398
|
+
width: dimensions?.width,
|
|
399
|
+
height: dimensions?.height
|
|
400
|
+
};
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (options.requireCalibration !== false) {
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
width: screen.screenWidth,
|
|
407
|
+
height: screen.screenHeight
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function saveNutImageObject(nut, image, options = {}) {
|
|
413
|
+
const path =
|
|
414
|
+
options.screenshotPath ?? join(tmpdir(), `automify-nut-${Date.now()}-${Math.random().toString(16).slice(2)}.png`);
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
if (nut.saveImage.length <= 1) {
|
|
418
|
+
await nut.saveImage({ image, path });
|
|
419
|
+
} else {
|
|
420
|
+
await nut.saveImage(image, path);
|
|
421
|
+
}
|
|
422
|
+
return await readFile(path);
|
|
423
|
+
} finally {
|
|
424
|
+
if (!options.screenshotPath) {
|
|
425
|
+
await unlink(path).catch(() => {});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function importNut() {
|
|
431
|
+
try {
|
|
432
|
+
return await import("@nut-tree/nut-js");
|
|
433
|
+
} catch (error) {
|
|
434
|
+
throw new AutomifyError(
|
|
435
|
+
"createLocalDesktopComputer requires the local desktop adapter dependency built from source. Install it with: npm run install:desktop",
|
|
436
|
+
{ cause: error }
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function maybeCall(fn, thisArg) {
|
|
442
|
+
if (typeof fn !== "function") return undefined;
|
|
443
|
+
return fn.call(thisArg);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function getMacOSDisplayInfo(options, environment) {
|
|
447
|
+
if (environment !== "mac" || options.macosDisplayInfo === false) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (options.macosDisplayInfo && typeof options.macosDisplayInfo === "object") {
|
|
452
|
+
return options.macosDisplayInfo;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const script = `
|
|
457
|
+
ObjC.import("AppKit");
|
|
458
|
+
const screen = $.NSScreen.mainScreen;
|
|
459
|
+
if (!screen) {
|
|
460
|
+
JSON.stringify(null);
|
|
461
|
+
} else {
|
|
462
|
+
const frame = screen.frame;
|
|
463
|
+
const visible = screen.visibleFrame;
|
|
464
|
+
JSON.stringify({
|
|
465
|
+
width: frame.size.width,
|
|
466
|
+
height: frame.size.height,
|
|
467
|
+
visibleX: visible.origin.x,
|
|
468
|
+
visibleY: visible.origin.y,
|
|
469
|
+
visibleWidth: visible.size.width,
|
|
470
|
+
visibleHeight: visible.size.height,
|
|
471
|
+
backingScaleFactor: screen.backingScaleFactor
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
`;
|
|
475
|
+
const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", script], { timeout: 1500 });
|
|
476
|
+
const parsed = JSON.parse(stdout.trim());
|
|
477
|
+
return parsed && parsed.width > 0 && parsed.height > 0 ? parsed : null;
|
|
478
|
+
} catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function defaultDesktopEnvironment() {
|
|
484
|
+
if (process.platform === "darwin") return "mac";
|
|
485
|
+
if (process.platform === "win32") return "windows";
|
|
486
|
+
return "ubuntu";
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function ensureLinuxVirtualDisplay(options = {}) {
|
|
490
|
+
if (process.platform !== "linux" || options.virtualDisplay === false) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (options.nut && options.forceVirtualDisplay !== true) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const env = options.env ?? process.env;
|
|
498
|
+
if (env.DISPLAY && options.forceVirtualDisplay !== true) {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const display = normalizeXDisplay(options.display ?? options.virtualDisplayDisplay ?? ":99");
|
|
503
|
+
const width = positiveInteger(options.virtualDisplayWidth) ?? positiveInteger(options.displayWidth) ?? 1440;
|
|
504
|
+
const height = positiveInteger(options.virtualDisplayHeight) ?? positiveInteger(options.displayHeight) ?? 900;
|
|
505
|
+
const depth = positiveInteger(options.virtualDisplayDepth) ?? 24;
|
|
506
|
+
const command = options.virtualDisplayCommand ?? "Xvfb";
|
|
507
|
+
const args = options.virtualDisplayArgs ?? [
|
|
508
|
+
display,
|
|
509
|
+
"-screen",
|
|
510
|
+
"0",
|
|
511
|
+
`${width}x${height}x${depth}`,
|
|
512
|
+
"-nolisten",
|
|
513
|
+
"tcp"
|
|
514
|
+
];
|
|
515
|
+
const spawnImpl = options.spawn ?? spawn;
|
|
516
|
+
|
|
517
|
+
debugLocalDesktop(options, "virtual_display_start", { command, args, display, width, height, depth });
|
|
518
|
+
const child = spawnImpl(command, args, {
|
|
519
|
+
detached: true,
|
|
520
|
+
stdio: "ignore",
|
|
521
|
+
env: {
|
|
522
|
+
...process.env,
|
|
523
|
+
...env
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
await waitForSpawn(child, options.virtualDisplayStartupMs ?? 250, command);
|
|
528
|
+
child.unref?.();
|
|
529
|
+
env.DISPLAY = display;
|
|
530
|
+
debugLocalDesktop(options, "virtual_display_ready", { display, pid: child.pid });
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
display,
|
|
534
|
+
process: child,
|
|
535
|
+
async close() {
|
|
536
|
+
if (!child.killed) {
|
|
537
|
+
child.kill("SIGTERM");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function waitForSpawn(child, startupMs, command) {
|
|
544
|
+
return new Promise((resolve, reject) => {
|
|
545
|
+
let settled = false;
|
|
546
|
+
const timer = setTimeout(
|
|
547
|
+
() => {
|
|
548
|
+
settled = true;
|
|
549
|
+
cleanup();
|
|
550
|
+
resolve();
|
|
551
|
+
},
|
|
552
|
+
Math.max(0, Number(startupMs) || 0)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
function cleanup() {
|
|
556
|
+
clearTimeout(timer);
|
|
557
|
+
child.off?.("error", onError);
|
|
558
|
+
child.off?.("exit", onExit);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function onError(error) {
|
|
562
|
+
if (settled) return;
|
|
563
|
+
settled = true;
|
|
564
|
+
cleanup();
|
|
565
|
+
reject(
|
|
566
|
+
new AutomifyError(
|
|
567
|
+
`Unable to start Linux virtual display with ${command}. Install Xvfb or pass virtualDisplay: false to use an existing DISPLAY.`,
|
|
568
|
+
{ cause: error }
|
|
569
|
+
)
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function onExit(code) {
|
|
574
|
+
if (settled) return;
|
|
575
|
+
settled = true;
|
|
576
|
+
cleanup();
|
|
577
|
+
reject(new AutomifyError(`Linux virtual display exited during startup with code ${code}.`));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
child.once?.("error", onError);
|
|
581
|
+
child.once?.("exit", onExit);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function normalizeXDisplay(display) {
|
|
586
|
+
const value = String(display || ":99").trim();
|
|
587
|
+
return value.startsWith(":") ? value : `:${value}`;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function nutButton(nut, button) {
|
|
591
|
+
if (button === "right") return nut.Button?.RIGHT ?? nut.Button?.Right ?? "right";
|
|
592
|
+
if (button === "middle") return nut.Button?.MIDDLE ?? nut.Button?.Middle ?? "middle";
|
|
593
|
+
return nut.Button?.LEFT ?? nut.Button?.Left ?? "left";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function moveMouse(nut, x, y, options = {}) {
|
|
597
|
+
const target = point(nut, x, y, options);
|
|
598
|
+
const transform = describePointTransform(
|
|
599
|
+
x,
|
|
600
|
+
y,
|
|
601
|
+
options.coordinateSpace
|
|
602
|
+
? {
|
|
603
|
+
...options.coordinateSpace,
|
|
604
|
+
actionType: options.actionType
|
|
605
|
+
}
|
|
606
|
+
: null
|
|
607
|
+
);
|
|
608
|
+
debugLocalDesktop(options, "move", {
|
|
609
|
+
...transform,
|
|
610
|
+
output: { x: target.x, y: target.y },
|
|
611
|
+
actionType: options.actionType
|
|
612
|
+
});
|
|
613
|
+
if (options.smoothMouseMove !== true && typeof nut.mouse.setPosition === "function") {
|
|
614
|
+
await nut.mouse.setPosition(target);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
await nut.mouse.move(nut.straightTo(target));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function dragMouse(nut, action, options = {}) {
|
|
621
|
+
const start = action.path?.[0] ?? action;
|
|
622
|
+
const end = action.path?.at(-1) ?? action;
|
|
623
|
+
await moveMouse(nut, start.x, start.y, options);
|
|
624
|
+
await nut.mouse.drag(nut.straightTo(point(nut, end.x, end.y, options)));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function scrollMouse(nut, action) {
|
|
628
|
+
const scrollY = Number(action.scroll_y ?? action.delta_y ?? action.deltaY ?? 0);
|
|
629
|
+
const scrollX = Number(action.scroll_x ?? action.delta_x ?? action.deltaX ?? 0);
|
|
630
|
+
const amount = Math.max(1, Math.ceil(Math.max(Math.abs(scrollY), Math.abs(scrollX)) / 120));
|
|
631
|
+
|
|
632
|
+
if (Math.abs(scrollX) > Math.abs(scrollY) && scrollX !== 0) {
|
|
633
|
+
if (scrollX > 0 && typeof nut.mouse.scrollRight === "function") return nut.mouse.scrollRight(amount);
|
|
634
|
+
if (scrollX < 0 && typeof nut.mouse.scrollLeft === "function") return nut.mouse.scrollLeft(amount);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (scrollY > 0) return nut.mouse.scrollDown(amount);
|
|
638
|
+
if (scrollY < 0) return nut.mouse.scrollUp(amount);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function pressKeys(nut, keys, options = {}) {
|
|
642
|
+
if (isMacCommandTab(keys, options)) {
|
|
643
|
+
await pressMacCommandTab(nut, options);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const normalized = keys.map((key) => nutKey(nut, key)).filter((key) => key != null);
|
|
648
|
+
if (normalized.length === 0) {
|
|
649
|
+
throw new AutomifyError("keypress action did not include any keys.");
|
|
650
|
+
}
|
|
651
|
+
await nut.keyboard.pressKey(...normalized);
|
|
652
|
+
if (typeof nut.keyboard.releaseKey === "function") {
|
|
653
|
+
await nut.keyboard.releaseKey(...normalized);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function pressMacCommandTab(nut, options) {
|
|
658
|
+
const command = nut.Key?.LeftCmd ?? nut.Key?.LeftSuper ?? "cmd";
|
|
659
|
+
const tab = nut.Key?.Tab ?? "tab";
|
|
660
|
+
const holdMs = Math.max(0, Number(options.macCommandTabHoldMs ?? 500) || 0);
|
|
661
|
+
const settleMs = Math.max(0, Number(options.macCommandTabSettleMs ?? 80) || 0);
|
|
662
|
+
|
|
663
|
+
debugLocalDesktop(options, "keyboard", {
|
|
664
|
+
method: "mac_command_tab",
|
|
665
|
+
holdMs,
|
|
666
|
+
settleMs
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
await nut.keyboard.pressKey(command);
|
|
670
|
+
if (settleMs > 0) {
|
|
671
|
+
await delay(settleMs);
|
|
672
|
+
}
|
|
673
|
+
await nut.keyboard.pressKey(tab);
|
|
674
|
+
if (settleMs > 0) {
|
|
675
|
+
await delay(settleMs);
|
|
676
|
+
}
|
|
677
|
+
if (typeof nut.keyboard.releaseKey === "function") {
|
|
678
|
+
await nut.keyboard.releaseKey(tab);
|
|
679
|
+
}
|
|
680
|
+
if (holdMs > 0) {
|
|
681
|
+
await delay(holdMs);
|
|
682
|
+
}
|
|
683
|
+
if (typeof nut.keyboard.releaseKey === "function") {
|
|
684
|
+
await nut.keyboard.releaseKey(command);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isMacCommandTab(keys, options = {}) {
|
|
689
|
+
const environment = options.environment ?? options.coordinateSpace?.environment ?? defaultDesktopEnvironment();
|
|
690
|
+
if (environment !== "mac") return false;
|
|
691
|
+
|
|
692
|
+
const normalized = new Set(keys.map((key) => String(key).trim().toLowerCase().replace(/\s+/g, "_")).filter(Boolean));
|
|
693
|
+
return normalized.has("tab") && ["cmd", "command", "meta"].some((key) => normalized.has(key));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function nutKey(nut, key) {
|
|
697
|
+
const raw = String(key);
|
|
698
|
+
const normalized = raw.trim().toLowerCase().replace(/\s+/g, "_");
|
|
699
|
+
const alias = KEY_ALIASES.get(normalized);
|
|
700
|
+
if (alias && nut.Key?.[alias] != null) return nut.Key[alias];
|
|
701
|
+
if (raw.length === 1) {
|
|
702
|
+
const upper = raw.toUpperCase();
|
|
703
|
+
return nut.Key?.[upper] ?? raw;
|
|
704
|
+
}
|
|
705
|
+
return nut.Key?.[raw] ?? nut.Key?.[capitalize(normalized)] ?? raw;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function point(nut, x, y, options = {}) {
|
|
709
|
+
const coordinateSpace = options.coordinateSpace
|
|
710
|
+
? {
|
|
711
|
+
...options.coordinateSpace,
|
|
712
|
+
actionType: options.actionType
|
|
713
|
+
}
|
|
714
|
+
: null;
|
|
715
|
+
const scaled = scalePoint(x, y, coordinateSpace);
|
|
716
|
+
const safePoint = {
|
|
717
|
+
x: Math.max(0, Math.round(scaled.x)),
|
|
718
|
+
y: Math.max(0, Math.round(scaled.y))
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
if (typeof nut.Point === "function") {
|
|
722
|
+
return new nut.Point(safePoint.x, safePoint.y);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return safePoint;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function scalePoint(x, y, coordinateSpace) {
|
|
729
|
+
const point = {
|
|
730
|
+
x: Number(x) || 0,
|
|
731
|
+
y: Number(y) || 0
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
if (!coordinateSpace) return point;
|
|
735
|
+
|
|
736
|
+
const scaled = {
|
|
737
|
+
x: point.x * coordinateSpace.mouseScaleX + coordinateSpace.mouseOffsetX,
|
|
738
|
+
y: point.y * coordinateSpace.mouseScaleY + coordinateSpace.mouseOffsetY
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
return scaled;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function describePointTransform(x, y, coordinateSpace) {
|
|
745
|
+
if (!coordinateSpace) {
|
|
746
|
+
return {
|
|
747
|
+
input: { x, y },
|
|
748
|
+
output: { x: Number(x) || 0, y: Number(y) || 0 },
|
|
749
|
+
coordinateSpace: null
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const input = {
|
|
754
|
+
x: Number(x) || 0,
|
|
755
|
+
y: Number(y) || 0
|
|
756
|
+
};
|
|
757
|
+
const scaled = {
|
|
758
|
+
x: input.x * coordinateSpace.mouseScaleX + coordinateSpace.mouseOffsetX,
|
|
759
|
+
y: input.y * coordinateSpace.mouseScaleY + coordinateSpace.mouseOffsetY
|
|
760
|
+
};
|
|
761
|
+
return {
|
|
762
|
+
input,
|
|
763
|
+
scaled,
|
|
764
|
+
output: scaled,
|
|
765
|
+
coordinateSpace: summarizeCoordinateSpace(coordinateSpace)
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function buildCoordinateSpace(options, screen) {
|
|
770
|
+
const macOSDisplay = screen.macOSDisplay;
|
|
771
|
+
const pixelScale =
|
|
772
|
+
positiveNumber(options.pixelScale) ??
|
|
773
|
+
positiveNumber(macOSDisplay?.backingScaleFactor) ??
|
|
774
|
+
inferPixelScale(screen, options);
|
|
775
|
+
const defaultScale = 1 / pixelScale;
|
|
776
|
+
const mouseScaleX = scaleRatio(macOSDisplay?.width, screen.displayWidth) ?? defaultScale;
|
|
777
|
+
const mouseScaleY = scaleRatio(macOSDisplay?.height, screen.displayHeight) ?? defaultScale;
|
|
778
|
+
const mouseWidth = positiveNumber(macOSDisplay?.width) ?? positiveNumber(screen.displayWidth) * mouseScaleX;
|
|
779
|
+
const mouseHeight = positiveNumber(macOSDisplay?.height) ?? positiveNumber(screen.displayHeight) * mouseScaleY;
|
|
780
|
+
|
|
781
|
+
return {
|
|
782
|
+
...screen,
|
|
783
|
+
pixelScale,
|
|
784
|
+
mouseWidth,
|
|
785
|
+
mouseHeight,
|
|
786
|
+
mouseScaleX: positiveNumber(options.mouseScaleX) ?? mouseScaleX,
|
|
787
|
+
mouseScaleY: positiveNumber(options.mouseScaleY) ?? mouseScaleY,
|
|
788
|
+
mouseOffsetX: finiteNumber(options.mouseOffsetX) ?? 0,
|
|
789
|
+
mouseOffsetY: finiteNumber(options.mouseOffsetY) ?? 0,
|
|
790
|
+
actionType: options.actionType
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function scaleRatio(target, source) {
|
|
795
|
+
const numericTarget = positiveNumber(target);
|
|
796
|
+
const numericSource = positiveNumber(source);
|
|
797
|
+
if (!numericTarget || !numericSource) return null;
|
|
798
|
+
return numericTarget / numericSource;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function inferPixelScale({ displayWidth, displayHeight, screenWidth, screenHeight }, options = {}) {
|
|
802
|
+
const environment = options.environment ?? defaultDesktopEnvironment();
|
|
803
|
+
if (environment !== "mac") return 1;
|
|
804
|
+
|
|
805
|
+
const width = positiveNumber(displayWidth) ?? positiveNumber(screenWidth);
|
|
806
|
+
const height = positiveNumber(displayHeight) ?? positiveNumber(screenHeight);
|
|
807
|
+
if (!width || !height) return 1;
|
|
808
|
+
|
|
809
|
+
// libnut reports macOS screen size in backing pixels, while CGEvent mouse
|
|
810
|
+
// coordinates use logical points. Built-in Retina displays are 2x here.
|
|
811
|
+
if (width >= 2000 || height >= 1400) return 2;
|
|
812
|
+
return 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function positiveNumber(value) {
|
|
816
|
+
const number = Number(value);
|
|
817
|
+
return Number.isFinite(number) && number > 0 ? number : null;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function positiveInteger(value) {
|
|
821
|
+
const number = Number(value);
|
|
822
|
+
if (!Number.isFinite(number) || number <= 0) return null;
|
|
823
|
+
return Math.floor(number);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function finiteNumber(value) {
|
|
827
|
+
const number = Number(value);
|
|
828
|
+
return Number.isFinite(number) ? number : null;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function configureNutMouse(nut, options = {}) {
|
|
832
|
+
if (!nut.mouse?.config || options.configureMouse === false) return;
|
|
833
|
+
|
|
834
|
+
if (options.mouseAutoDelayMs != null) {
|
|
835
|
+
nut.mouse.config.autoDelayMs = Math.max(0, Number(options.mouseAutoDelayMs) || 0);
|
|
836
|
+
} else if (nut.mouse.config.autoDelayMs == null || nut.mouse.config.autoDelayMs > 0) {
|
|
837
|
+
nut.mouse.config.autoDelayMs = 0;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (options.mouseSpeed != null) {
|
|
841
|
+
nut.mouse.config.mouseSpeed = Math.max(0, Number(options.mouseSpeed) || 0);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function configureNutKeyboard(nut, options = {}) {
|
|
846
|
+
if (!nut.keyboard?.config || options.configureKeyboard === false) return;
|
|
847
|
+
|
|
848
|
+
if (options.keyboardAutoDelayMs != null) {
|
|
849
|
+
nut.keyboard.config.autoDelayMs = Math.max(0, Number(options.keyboardAutoDelayMs) || 0);
|
|
850
|
+
} else if (nut.keyboard.config.autoDelayMs == null || nut.keyboard.config.autoDelayMs > 0) {
|
|
851
|
+
nut.keyboard.config.autoDelayMs = 0;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function debugLocalDesktop(options, message, details) {
|
|
856
|
+
writeDebugLogFile(options.logFile, "automify:local-desktop", message, details, { silent: options.silent });
|
|
857
|
+
if (options.silent || !options.debug) return;
|
|
858
|
+
const label = `[automify:local-desktop] ${message}`;
|
|
859
|
+
if (typeof options.debug === "function") {
|
|
860
|
+
options.debug(label, details);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
console.error(formatDesktopLog(label, details));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function formatDesktopLog(label, details) {
|
|
867
|
+
if (!details || typeof details !== "object") return label;
|
|
868
|
+
const parts = [];
|
|
869
|
+
const add = (key, value) => {
|
|
870
|
+
if (value == null || value === "") return;
|
|
871
|
+
parts.push(`${key}=${value}`);
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
add("action", describeDesktopAction(details.action));
|
|
875
|
+
if (details.input) add("input", `${details.input.x ?? "?"},${details.input.y ?? "?"}`);
|
|
876
|
+
if (details.output) add("output", `${details.output.x ?? "?"},${details.output.y ?? "?"}`);
|
|
877
|
+
add("actionType", details.actionType);
|
|
878
|
+
add("method", details.method);
|
|
879
|
+
add("holdMs", details.holdMs);
|
|
880
|
+
add("settleMs", details.settleMs);
|
|
881
|
+
add("bytes", details.bytes);
|
|
882
|
+
add("durationMs", details.durationMs);
|
|
883
|
+
add("environment", details.environment);
|
|
884
|
+
if (details.displayWidth && details.displayHeight) add("display", `${details.displayWidth}x${details.displayHeight}`);
|
|
885
|
+
if (details.screenWidth && details.screenHeight) add("screen", `${details.screenWidth}x${details.screenHeight}`);
|
|
886
|
+
if (details.coordinateSpace?.mouseScaleX != null || details.coordinateSpace?.mouseScaleY != null) {
|
|
887
|
+
add("mouseScale", `${details.coordinateSpace.mouseScaleX ?? "?"},${details.coordinateSpace.mouseScaleY ?? "?"}`);
|
|
888
|
+
}
|
|
889
|
+
if (details.coordinateSpace?.mouseOffsetX != null || details.coordinateSpace?.mouseOffsetY != null) {
|
|
890
|
+
add("mouseOffset", `${details.coordinateSpace.mouseOffsetX ?? "?"},${details.coordinateSpace.mouseOffsetY ?? "?"}`);
|
|
891
|
+
}
|
|
892
|
+
if (details.coordinateSpace?.pixelScale != null) add("pixelScale", details.coordinateSpace.pixelScale);
|
|
893
|
+
|
|
894
|
+
return parts.length ? `${label} ${parts.join(" ")}` : label;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function describeDesktopAction(action) {
|
|
898
|
+
if (!action?.type) return "";
|
|
899
|
+
const parts = [action.type];
|
|
900
|
+
if (action.x != null || action.y != null) parts.push(`@${action.x ?? "?"},${action.y ?? "?"}`);
|
|
901
|
+
if (action.button) parts.push(`button:${action.button}`);
|
|
902
|
+
const keys = action.keys ?? [action.key].filter(Boolean);
|
|
903
|
+
if (keys?.length) parts.push(`keys:${keys.join("+")}`);
|
|
904
|
+
if (action.text != null) parts.push(`text:${JSON.stringify(String(action.text).slice(0, 80))}`);
|
|
905
|
+
if (action.ms != null || action.duration_ms != null) parts.push(`ms:${action.ms ?? action.duration_ms}`);
|
|
906
|
+
if (action.scroll_x != null || action.scroll_y != null)
|
|
907
|
+
parts.push(`scroll:${action.scroll_x ?? 0},${action.scroll_y ?? 0}`);
|
|
908
|
+
if (action.delta_x != null || action.delta_y != null)
|
|
909
|
+
parts.push(`delta:${action.delta_x ?? 0},${action.delta_y ?? 0}`);
|
|
910
|
+
return parts.join(":");
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function summarizeCoordinateSpace(coordinateSpace) {
|
|
914
|
+
if (!coordinateSpace) return null;
|
|
915
|
+
return {
|
|
916
|
+
environment: coordinateSpace.environment,
|
|
917
|
+
displayWidth: coordinateSpace.displayWidth,
|
|
918
|
+
displayHeight: coordinateSpace.displayHeight,
|
|
919
|
+
screenWidth: coordinateSpace.screenWidth,
|
|
920
|
+
screenHeight: coordinateSpace.screenHeight,
|
|
921
|
+
mouseWidth: coordinateSpace.mouseWidth,
|
|
922
|
+
mouseHeight: coordinateSpace.mouseHeight,
|
|
923
|
+
pixelScale: coordinateSpace.pixelScale,
|
|
924
|
+
mouseScaleX: coordinateSpace.mouseScaleX,
|
|
925
|
+
mouseScaleY: coordinateSpace.mouseScaleY,
|
|
926
|
+
mouseOffsetX: coordinateSpace.mouseOffsetX,
|
|
927
|
+
mouseOffsetY: coordinateSpace.mouseOffsetY
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function pngDimensions(bytes) {
|
|
932
|
+
const buffer = Buffer.from(bytes);
|
|
933
|
+
if (buffer.length < 24) return null;
|
|
934
|
+
if (
|
|
935
|
+
buffer[0] !== 0x89 ||
|
|
936
|
+
buffer[1] !== 0x50 ||
|
|
937
|
+
buffer[2] !== 0x4e ||
|
|
938
|
+
buffer[3] !== 0x47 ||
|
|
939
|
+
buffer[4] !== 0x0d ||
|
|
940
|
+
buffer[5] !== 0x0a ||
|
|
941
|
+
buffer[6] !== 0x1a ||
|
|
942
|
+
buffer[7] !== 0x0a
|
|
943
|
+
) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return {
|
|
948
|
+
width: buffer.readUInt32BE(16),
|
|
949
|
+
height: buffer.readUInt32BE(20)
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function capitalize(value) {
|
|
954
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function isByteLike(value) {
|
|
958
|
+
return Buffer.isBuffer(value) || value instanceof Uint8Array || value instanceof ArrayBuffer;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function isImageObject(value) {
|
|
962
|
+
return value && typeof value === "object" && !isByteLike(value);
|
|
963
|
+
}
|