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.
Files changed (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +401 -0
  4. package/SECURITY.md +17 -0
  5. package/examples/anthropic-provider.js +18 -0
  6. package/examples/browser-basic.js +30 -0
  7. package/examples/browser-with-safety.js +38 -0
  8. package/examples/claude-model-adapter.js +141 -0
  9. package/examples/cli-basic.js +20 -0
  10. package/examples/cli-docker.js +42 -0
  11. package/examples/custom-computer.js +18 -0
  12. package/examples/custom-model-adapter.js +48 -0
  13. package/examples/desktop-docker.js +37 -0
  14. package/examples/desktop-local.js +28 -0
  15. package/examples/evaluate-image.js +26 -0
  16. package/examples/files-and-shared-folder.js +42 -0
  17. package/package.json +74 -0
  18. package/scripts/generate-argument-reference.js +17 -0
  19. package/scripts/install-browser.js +12 -0
  20. package/scripts/install-desktop.js +281 -0
  21. package/src/index.d.ts +1049 -0
  22. package/src/index.js +83 -0
  23. package/src/lib/adapter-locks.js +93 -0
  24. package/src/lib/adapter-toolkit.js +239 -0
  25. package/src/lib/anthropic-model-adapter.js +451 -0
  26. package/src/lib/argument-reference.js +98 -0
  27. package/src/lib/automify.js +938 -0
  28. package/src/lib/browser-automify.js +89 -0
  29. package/src/lib/cli-automify.js +520 -0
  30. package/src/lib/computer-automify.js +103 -0
  31. package/src/lib/docker-cli-automify.js +517 -0
  32. package/src/lib/docker-desktop-computer.js +725 -0
  33. package/src/lib/errors.js +24 -0
  34. package/src/lib/file-data.js +140 -0
  35. package/src/lib/init.js +217 -0
  36. package/src/lib/local-desktop-computer.js +963 -0
  37. package/src/lib/model-adapter.js +32 -0
  38. package/src/lib/openai-responses-client.js +162 -0
  39. package/src/lib/output.js +57 -0
  40. package/src/lib/playwright-computer.js +363 -0
  41. package/src/lib/presets.js +141 -0
  42. package/src/lib/result.js +95 -0
  43. package/src/lib/runtime.js +471 -0
  44. package/src/lib/virtual-shared-folder.js +109 -0
  45. package/src/lib/zod-output.js +26 -0
  46. package/src/zod.d.ts +12 -0
  47. package/src/zod.js +5 -0
@@ -0,0 +1,32 @@
1
+ import { AutomifyError } from "./errors.js";
2
+
3
+ export function createModelAdapter(adapter, config = {}) {
4
+ if (typeof adapter === "function") {
5
+ return createModelAdapter(adapter(config), config);
6
+ }
7
+
8
+ if (!adapter || typeof adapter !== "object") {
9
+ throw new AutomifyError("A model adapter object is required.");
10
+ }
11
+
12
+ if (typeof adapter.create === "function") {
13
+ return createModelAdapter(adapter.create({ ...config, ...(adapter.options ?? {}) }), {
14
+ ...config,
15
+ ...(adapter.options ?? {})
16
+ });
17
+ }
18
+
19
+ if (typeof adapter.createResponse === "function") {
20
+ return adapter;
21
+ }
22
+
23
+ if (typeof adapter.respond === "function") {
24
+ return {
25
+ createResponse(payload, context) {
26
+ return adapter.respond(payload, context);
27
+ }
28
+ };
29
+ }
30
+
31
+ throw new AutomifyError("A model adapter must provide createResponse(payload) or respond(payload).");
32
+ }
@@ -0,0 +1,162 @@
1
+ import { AutomifyError } from "./errors.js";
2
+
3
+ const DEFAULT_BASE_URL = "https://api.openai.com/v1";
4
+ const DEFAULT_TIMEOUT_MS = 120_000;
5
+ const DEFAULT_MAX_RETRIES = 2;
6
+
7
+ export class OpenAIResponsesClient {
8
+ constructor({
9
+ openaiApiKey,
10
+ baseURL = DEFAULT_BASE_URL,
11
+ fetchImpl = globalThis.fetch,
12
+ timeoutMs = DEFAULT_TIMEOUT_MS,
13
+ maxRetries = DEFAULT_MAX_RETRIES,
14
+ retryDelayMs = 500
15
+ } = {}) {
16
+ this.openaiApiKey = openaiApiKey;
17
+ this.baseURL = baseURL.replace(/\/$/, "");
18
+ this.fetch = fetchImpl;
19
+ this.timeoutMs = timeoutMs;
20
+ this.maxRetries = maxRetries;
21
+ this.retryDelayMs = retryDelayMs;
22
+
23
+ if (!this.openaiApiKey) {
24
+ throw new AutomifyError("An openaiApiKey is required.");
25
+ }
26
+
27
+ if (typeof this.fetch !== "function") {
28
+ throw new AutomifyError("A fetch implementation is required. Use Node 18+ or pass fetchImpl.");
29
+ }
30
+ }
31
+
32
+ async createResponse(payload) {
33
+ let lastError;
34
+
35
+ for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
36
+ try {
37
+ const response = await this.#fetchResponse(payload);
38
+ const text = await response.text();
39
+ const data = parseJson(text);
40
+
41
+ if (!response.ok) {
42
+ const message = data?.error?.message ?? data?.message ?? response.statusText;
43
+ const requestId = response.headers?.get?.("x-request-id") ?? response.headers?.get?.("openai-request-id");
44
+ const error = new AutomifyError(`OpenAI Responses request failed${requestId ? ` (${requestId})` : ""}: ${message}`);
45
+ error.status = response.status;
46
+ error.requestId = requestId;
47
+ if (attempt < this.maxRetries && isRetryableStatus(response.status)) {
48
+ lastError = error;
49
+ await wait(retryDelay(attempt, this.retryDelayMs, response.headers?.get?.("retry-after")));
50
+ continue;
51
+ }
52
+ throw error;
53
+ }
54
+
55
+ return data;
56
+ } catch (error) {
57
+ lastError = error;
58
+ if (attempt >= this.maxRetries || !isRetryableError(error)) {
59
+ throw error;
60
+ }
61
+ await wait(retryDelay(attempt, this.retryDelayMs));
62
+ }
63
+ }
64
+
65
+ throw lastError;
66
+ }
67
+
68
+ async #fetchResponse(payload) {
69
+ const controller = new AbortController();
70
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
71
+ const requestPayload = toOpenAIResponsesPayload(payload);
72
+
73
+ try {
74
+ return await this.fetch(`${this.baseURL}/responses`, {
75
+ method: "POST",
76
+ headers: {
77
+ authorization: `Bearer ${this.openaiApiKey}`,
78
+ "content-type": "application/json"
79
+ },
80
+ body: JSON.stringify(requestPayload),
81
+ signal: controller.signal
82
+ });
83
+ } finally {
84
+ clearTimeout(timeout);
85
+ }
86
+ }
87
+ }
88
+
89
+ function toOpenAIResponsesPayload(payload) {
90
+ if (!Array.isArray(payload?.tools)) return payload;
91
+
92
+ let changed = false;
93
+ const tools = payload.tools.map((tool) => {
94
+ const next = toOpenAIResponsesTool(tool, payload);
95
+ if (next !== tool) changed = true;
96
+ return next;
97
+ });
98
+
99
+ return changed ? { ...payload, tools } : payload;
100
+ }
101
+
102
+ function toOpenAIResponsesTool(tool, payload) {
103
+ if (!tool || (tool.type !== "computer" && tool.type !== "computer_use_preview")) {
104
+ return tool;
105
+ }
106
+
107
+ if (payload?.model === "computer-use-preview") {
108
+ return toOpenAIPreviewComputerTool(tool);
109
+ }
110
+
111
+ return toOpenAIGaComputerTool(tool);
112
+ }
113
+
114
+ function toOpenAIGaComputerTool(tool) {
115
+ const { displayWidth, displayHeight, display_width, display_height, environment, ...rest } = tool;
116
+ return {
117
+ ...rest,
118
+ type: "computer"
119
+ };
120
+ }
121
+
122
+ function toOpenAIPreviewComputerTool(tool) {
123
+ const { displayWidth, displayHeight, display_width, display_height, ...rest } = tool;
124
+ return cleanUndefined({
125
+ ...rest,
126
+ type: "computer_use_preview",
127
+ display_width: display_width ?? displayWidth,
128
+ display_height: display_height ?? displayHeight
129
+ });
130
+ }
131
+
132
+ function parseJson(text) {
133
+ if (!text) return null;
134
+
135
+ try {
136
+ return JSON.parse(text);
137
+ } catch (error) {
138
+ throw new AutomifyError("OpenAI Responses request returned invalid JSON.", { cause: error });
139
+ }
140
+ }
141
+
142
+ function isRetryableStatus(status) {
143
+ return status === 408 || status === 409 || status === 429 || status >= 500;
144
+ }
145
+
146
+ function isRetryableError(error) {
147
+ return error?.name === "AbortError" || error?.code === "ECONNRESET" || error?.code === "ETIMEDOUT" || /fetch failed/i.test(error?.message ?? "");
148
+ }
149
+
150
+ function retryDelay(attempt, baseDelayMs, retryAfter) {
151
+ const retryAfterMs = Number(retryAfter) * 1000;
152
+ if (Number.isFinite(retryAfterMs) && retryAfterMs > 0) return retryAfterMs;
153
+ return Math.max(0, Number(baseDelayMs) || 0) * (2 ** attempt);
154
+ }
155
+
156
+ function wait(ms) {
157
+ return new Promise((resolve) => setTimeout(resolve, ms));
158
+ }
159
+
160
+ function cleanUndefined(value) {
161
+ return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== undefined));
162
+ }
@@ -0,0 +1,57 @@
1
+ import { AutomifyError } from "./errors.js";
2
+
3
+ const PRIMITIVE_TYPES = new Set(["string", "number", "integer", "boolean", "object", "array", "null"]);
4
+
5
+ export function jsonOutput(name, shape, options = {}) {
6
+ if (typeof name !== "string" || name.trim() === "") {
7
+ throw new AutomifyError("jsonOutput requires a non-empty name.");
8
+ }
9
+
10
+ return {
11
+ type: "json_schema",
12
+ name,
13
+ schema: normalizeSchema(shape),
14
+ strict: options.strict ?? true,
15
+ description: options.description,
16
+ parse: options.parse
17
+ };
18
+ }
19
+
20
+ function normalizeSchema(shape) {
21
+ if (!shape || typeof shape !== "object" || Array.isArray(shape)) {
22
+ throw new AutomifyError("jsonOutput requires an object shape or JSON schema.");
23
+ }
24
+
25
+ if (shape.type === "object" && shape.properties) {
26
+ return shape;
27
+ }
28
+
29
+ const properties = {};
30
+
31
+ for (const [key, value] of Object.entries(shape)) {
32
+ properties[key] = normalizeProperty(value, key);
33
+ }
34
+
35
+ return {
36
+ type: "object",
37
+ properties,
38
+ required: Object.keys(properties),
39
+ additionalProperties: false
40
+ };
41
+ }
42
+
43
+ function normalizeProperty(value, key) {
44
+ if (typeof value === "string") {
45
+ if (!PRIMITIVE_TYPES.has(value)) {
46
+ throw new AutomifyError(`Unsupported jsonOutput type for "${key}": ${value}`);
47
+ }
48
+
49
+ return { type: value };
50
+ }
51
+
52
+ if (value && typeof value === "object" && !Array.isArray(value)) {
53
+ return value;
54
+ }
55
+
56
+ throw new AutomifyError(`jsonOutput field "${key}" must be a JSON type string or schema object.`);
57
+ }
@@ -0,0 +1,363 @@
1
+ import { AutomifyError } from "./errors.js";
2
+ import { assertKnownOptions, normalizeLogFile, writeDebugLogFile } from "./runtime.js";
3
+
4
+ const KEY_ALIASES = new Map([
5
+ ["alt", "Alt"],
6
+ ["cmd", "Meta"],
7
+ ["command", "Meta"],
8
+ ["control", "Control"],
9
+ ["ctrl", "Control"],
10
+ ["enter", "Enter"],
11
+ ["meta", "Meta"],
12
+ ["option", "Alt"],
13
+ ["shift", "Shift"],
14
+ ["space", " "],
15
+ ["tab", "Tab"],
16
+ ["escape", "Escape"],
17
+ ["esc", "Escape"],
18
+ ["backspace", "Backspace"],
19
+ ["delete", "Delete"],
20
+ ["arrowup", "ArrowUp"],
21
+ ["arrowdown", "ArrowDown"],
22
+ ["arrowleft", "ArrowLeft"],
23
+ ["arrowright", "ArrowRight"]
24
+ ]);
25
+ const DEFAULT_BROWSER_INSTRUCTIONS = [
26
+ "You are controlling a browser through screenshots and mouse/keyboard actions.",
27
+ "Orient from the screenshot and current URL first: identify the page, focused field, visible controls, and the specific target required by the task before acting.",
28
+ "Use deterministic browser controls. For a known URL, focus the address bar and type the URL instead of searching or clicking around. For content inside the current app, use visible search/filter fields, tabs, menus, or clear navigation controls.",
29
+ "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, links, fields, tabs, and menu items over unlabeled regions.",
30
+ "Do not navigate away from the current app unless the task requires it, provides a target URL/domain, or the current page is clearly unrelated. Respect any allowed-domain policy.",
31
+ "If the target is not visible, choose a deterministic recovery path: in-page search/filter, browser address bar for a known URL, visible navigation, scroll only when content is likely below, or wait only when loading is visible. Do not repeat nearly identical clicks after no visible change.",
32
+ "After any action that navigates, submits, opens a dialog, changes page state, 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."
33
+ ].join("\n");
34
+ const BROWSER_COMPUTER_OPTION_KEYS = new Set([
35
+ "playwright",
36
+ "browser",
37
+ "browserName",
38
+ "browserOptions",
39
+ "headless",
40
+ "startUrl",
41
+ "url",
42
+ "viewport",
43
+ "displayWidth",
44
+ "displayHeight",
45
+ "environment",
46
+ "launch",
47
+ "launchOptions",
48
+ "context",
49
+ "contextOptions",
50
+ "navigation",
51
+ "gotoOptions",
52
+ "actionDelayMs",
53
+ "waitMs",
54
+ "instructions",
55
+ "silent",
56
+ "debug",
57
+ "logFile",
58
+ "onUnknownAction"
59
+ ]);
60
+ const BROWSER_OPTIONS_KEYS = new Set(["name", "launch", "context", "navigation"]);
61
+
62
+ export async function createBrowserComputer(options = {}) {
63
+ options = normalizeBrowserComputerOptions(options);
64
+ const playwright = options.playwright ?? (await importPlaywright());
65
+ const browserName = options.browserName ?? "chromium";
66
+ const browserType = playwright[browserName];
67
+
68
+ if (!browserType || typeof browserType.launch !== "function") {
69
+ throw new AutomifyError(`Unsupported Playwright browserName: ${browserName}`);
70
+ }
71
+
72
+ const displayWidth = options.displayWidth ?? 1024;
73
+ const displayHeight = options.displayHeight ?? 768;
74
+ debugPlaywrightComputer(options, "setup_start", {
75
+ browserName,
76
+ headless: options.headless ?? true,
77
+ url: options.url,
78
+ width: displayWidth,
79
+ height: displayHeight
80
+ });
81
+ const browser = await browserType.launch({
82
+ headless: options.headless ?? true,
83
+ ...options.launchOptions
84
+ });
85
+
86
+ try {
87
+ const context = await browser.newContext({
88
+ viewport: { width: displayWidth, height: displayHeight },
89
+ ...options.contextOptions
90
+ });
91
+ const page = await context.newPage();
92
+
93
+ if (options.url) {
94
+ await page.goto(options.url, options.gotoOptions);
95
+ }
96
+ debugPlaywrightComputer(options, "setup_complete", {
97
+ browserName,
98
+ url: typeof page.url === "function" ? page.url() : options.url,
99
+ width: displayWidth,
100
+ height: displayHeight
101
+ });
102
+
103
+ const computer = createPlaywrightComputer(page, {
104
+ ...options,
105
+ displayWidth,
106
+ displayHeight
107
+ });
108
+
109
+ return {
110
+ ...computer,
111
+ browser,
112
+ context,
113
+ page,
114
+ async goto(url, gotoOptions = options.gotoOptions) {
115
+ await page.goto(url, gotoOptions);
116
+ },
117
+ async close() {
118
+ await browser.close();
119
+ }
120
+ };
121
+ } catch (error) {
122
+ await browser.close().catch(() => {});
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ export function createPlaywrightComputer(page, options = {}) {
128
+ options = normalizeBrowserComputerOptions(options);
129
+ return {
130
+ displayWidth: options.displayWidth ?? page.viewportSize()?.width ?? 1024,
131
+ displayHeight: options.displayHeight ?? page.viewportSize()?.height ?? 768,
132
+ environment: options.environment ?? "browser",
133
+ instructions: options.instructions ?? DEFAULT_BROWSER_INSTRUCTIONS,
134
+
135
+ async execute(action) {
136
+ await executePlaywrightAction(page, action, options);
137
+ },
138
+
139
+ async screenshot(context) {
140
+ const startedAt = Date.now();
141
+ if (context?.initial || context?.final) {
142
+ try {
143
+ await page.waitForLoadState("networkidle", { timeout: 2000 });
144
+ } catch {}
145
+ await page
146
+ .evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
147
+ .catch(() => {});
148
+ }
149
+ const screenshot = await page.screenshot({ fullPage: false, animations: "disabled" });
150
+ debugPlaywrightComputer(options, "screenshot", {
151
+ phase: context?.final ? "final" : context?.initial ? "initial" : "step",
152
+ bytes: screenshot?.byteLength,
153
+ durationMs: Date.now() - startedAt,
154
+ url: typeof page.url === "function" ? page.url() : undefined
155
+ });
156
+ return screenshot;
157
+ },
158
+
159
+ async currentUrl() {
160
+ return page.url();
161
+ }
162
+ };
163
+ }
164
+
165
+ function normalizeBrowserComputerOptions(options = {}) {
166
+ assertKnownOptions("browser adapter", options, BROWSER_COMPUTER_OPTION_KEYS);
167
+ assertKnownOptions("browserOptions", options.browserOptions, BROWSER_OPTIONS_KEYS);
168
+ const viewport = options.viewport ?? {};
169
+ const browserOptions = options.browserOptions ?? {};
170
+ return {
171
+ ...options,
172
+ debug: options.debug ?? false,
173
+ logFile: normalizeLogFile(options.logFile, "browser adapter logFile"),
174
+ browserName: options.browserName ?? options.browser ?? browserOptions.name,
175
+ url: options.url ?? options.startUrl,
176
+ displayWidth: options.displayWidth ?? viewport.width,
177
+ displayHeight: options.displayHeight ?? viewport.height,
178
+ launchOptions: options.launchOptions ?? options.launch ?? browserOptions.launch,
179
+ contextOptions: options.contextOptions ?? options.context ?? browserOptions.context,
180
+ gotoOptions: options.gotoOptions ?? options.navigation ?? browserOptions.navigation,
181
+ waitMs: options.waitMs ?? options.actionDelayMs
182
+ };
183
+ }
184
+
185
+ export async function executePlaywrightAction(page, action, options = {}) {
186
+ debugPlaywrightComputer(options, "action", { action });
187
+
188
+ switch (action.type) {
189
+ case "click":
190
+ debugPlaywrightComputer(options, "mouse", {
191
+ method: "click",
192
+ input: { x: action.x, y: action.y },
193
+ button: normalizeButton(action.button)
194
+ });
195
+ await page.mouse.click(action.x, action.y, {
196
+ button: normalizeButton(action.button)
197
+ });
198
+ break;
199
+ case "double_click":
200
+ debugPlaywrightComputer(options, "mouse", {
201
+ method: "double_click",
202
+ input: { x: action.x, y: action.y },
203
+ button: normalizeButton(action.button)
204
+ });
205
+ await page.mouse.dblclick(action.x, action.y, {
206
+ button: normalizeButton(action.button)
207
+ });
208
+ break;
209
+ case "scroll":
210
+ debugPlaywrightComputer(options, "mouse", {
211
+ method: "scroll",
212
+ input: { x: action.x, y: action.y },
213
+ scrollX: action.scroll_x ?? 0,
214
+ scrollY: action.scroll_y ?? 0
215
+ });
216
+ await page.mouse.move(action.x, action.y);
217
+ await page.evaluate(
218
+ ([scrollX, scrollY]) => window.scrollBy(scrollX, scrollY),
219
+ [action.scroll_x ?? 0, action.scroll_y ?? 0]
220
+ );
221
+ break;
222
+ case "keypress":
223
+ {
224
+ const keys = action.keys ?? [action.key].filter(Boolean);
225
+ if (keys.length === 0) {
226
+ throw new AutomifyError("keypress action did not include any keys.");
227
+ }
228
+ const output = normalizeKeypress(keys);
229
+ debugPlaywrightComputer(options, "keyboard", {
230
+ method: "press",
231
+ input: keys,
232
+ output
233
+ });
234
+ await page.keyboard.press(output);
235
+ }
236
+ break;
237
+ case "type":
238
+ debugPlaywrightComputer(options, "keyboard", {
239
+ method: "type",
240
+ text: action.text ?? ""
241
+ });
242
+ await page.keyboard.type(action.text ?? "");
243
+ break;
244
+ case "wait":
245
+ await page.waitForTimeout(options.waitMs ?? 1000);
246
+ break;
247
+ case "screenshot":
248
+ break;
249
+ case "move":
250
+ debugPlaywrightComputer(options, "mouse", {
251
+ method: "move",
252
+ input: { x: action.x, y: action.y }
253
+ });
254
+ await page.mouse.move(action.x, action.y);
255
+ break;
256
+ case "drag":
257
+ debugPlaywrightComputer(options, "mouse", {
258
+ method: "drag",
259
+ start: { x: action.x, y: action.y },
260
+ end: {
261
+ x: action.path?.at(-1)?.x ?? action.x,
262
+ y: action.path?.at(-1)?.y ?? action.y
263
+ }
264
+ });
265
+ await page.mouse.move(action.x, action.y);
266
+ await page.mouse.down();
267
+ await page.mouse.move(action.path?.at(-1)?.x ?? action.x, action.path?.at(-1)?.y ?? action.y);
268
+ await page.mouse.up();
269
+ break;
270
+ default:
271
+ if (typeof options.onUnknownAction === "function") {
272
+ await options.onUnknownAction(action);
273
+ }
274
+ }
275
+ }
276
+
277
+ function debugPlaywrightComputer(options, message, details) {
278
+ writeDebugLogFile(options.logFile, "automify:browser-computer", message, details, { silent: options.silent });
279
+ if (options.silent || !options.debug) return;
280
+ const label = `[automify:browser-computer] ${message}`;
281
+ if (typeof options.debug === "function") {
282
+ options.debug(label, details);
283
+ return;
284
+ }
285
+ console.error(formatBrowserLog(label, details));
286
+ }
287
+
288
+ function formatBrowserLog(label, details) {
289
+ if (!details || typeof details !== "object") return label;
290
+ const parts = [];
291
+ const add = (key, value) => {
292
+ if (value == null || value === "") return;
293
+ parts.push(`${key}=${value}`);
294
+ };
295
+
296
+ add("action", describeBrowserAction(details.action));
297
+ add("browser", details.browserName);
298
+ if (details.width && details.height) add("viewport", `${details.width}x${details.height}`);
299
+ if (details.headless != null) add("headless", details.headless);
300
+ add("method", details.method);
301
+ if (details.input)
302
+ add(
303
+ "input",
304
+ Array.isArray(details.input) ? details.input.join("+") : `${details.input.x ?? "?"},${details.input.y ?? "?"}`
305
+ );
306
+ if (details.output)
307
+ add("output", typeof details.output === "string" ? details.output : JSON.stringify(details.output));
308
+ if (details.start) add("start", `${details.start.x ?? "?"},${details.start.y ?? "?"}`);
309
+ if (details.end) add("end", `${details.end.x ?? "?"},${details.end.y ?? "?"}`);
310
+ add("button", details.button);
311
+ add(
312
+ "scroll",
313
+ details.scrollX != null || details.scrollY != null ? `${details.scrollX ?? 0},${details.scrollY ?? 0}` : undefined
314
+ );
315
+ if (details.text != null) add("text", JSON.stringify(String(details.text).slice(0, 80)));
316
+ add("phase", details.phase);
317
+ add("bytes", details.bytes);
318
+ add("durationMs", details.durationMs);
319
+ if (details.url) add("url", JSON.stringify(details.url));
320
+
321
+ return parts.length ? `${label} ${parts.join(" ")}` : label;
322
+ }
323
+
324
+ function describeBrowserAction(action) {
325
+ if (!action?.type) return "";
326
+ const parts = [action.type];
327
+ if (action.x != null || action.y != null) parts.push(`@${action.x ?? "?"},${action.y ?? "?"}`);
328
+ if (action.button) parts.push(`button:${action.button}`);
329
+ const keys = action.keys ?? [action.key].filter(Boolean);
330
+ if (keys?.length) parts.push(`keys:${keys.join("+")}`);
331
+ if (action.text != null) parts.push(`text:${JSON.stringify(String(action.text).slice(0, 80))}`);
332
+ if (action.ms != null || action.duration_ms != null) parts.push(`ms:${action.ms ?? action.duration_ms}`);
333
+ if (action.scroll_x != null || action.scroll_y != null)
334
+ parts.push(`scroll:${action.scroll_x ?? 0},${action.scroll_y ?? 0}`);
335
+ if (action.delta_x != null || action.delta_y != null)
336
+ parts.push(`delta:${action.delta_x ?? 0},${action.delta_y ?? 0}`);
337
+ return parts.join(":");
338
+ }
339
+
340
+ function normalizeButton(button) {
341
+ return button === "right" || button === "middle" ? button : "left";
342
+ }
343
+
344
+ function normalizeKey(key) {
345
+ return KEY_ALIASES.get(String(key).toLowerCase()) ?? key;
346
+ }
347
+
348
+ function normalizeKeypress(keys) {
349
+ return keys.map((key) => normalizeKey(key)).join("+");
350
+ }
351
+
352
+ async function importPlaywright() {
353
+ try {
354
+ return await import("playwright");
355
+ } catch (error) {
356
+ throw new AutomifyError(
357
+ "createBrowserComputer requires the 'playwright' dependency. Reinstall dependencies with: npm install",
358
+ {
359
+ cause: error
360
+ }
361
+ );
362
+ }
363
+ }