@opencoreai/opencore 0.2.2

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.
@@ -0,0 +1,614 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+ const API_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
9
+ const API_IMAGE_TARGET_BYTES = 4_900_000;
10
+
11
+ const SPECIAL_KEY_CODES = {
12
+ ARROWDOWN: 125,
13
+ ARROWLEFT: 123,
14
+ ARROWRIGHT: 124,
15
+ ARROWUP: 126,
16
+ BACKSPACE: 51,
17
+ DELETE: 117,
18
+ DOWN: 125,
19
+ DOWNARROW: 125,
20
+ END: 119,
21
+ ENTER: 36,
22
+ ESC: 53,
23
+ ESCAPE: 53,
24
+ HOME: 115,
25
+ LEFT: 123,
26
+ LEFTARROW: 123,
27
+ PAGEDN: 121,
28
+ PAGEDOWN: 121,
29
+ PAGEUP: 116,
30
+ PGDN: 121,
31
+ PGUP: 116,
32
+ RETURN: 36,
33
+ RIGHT: 124,
34
+ RIGHTARROW: 124,
35
+ SPACE: 49,
36
+ TAB: 48,
37
+ UP: 126,
38
+ UPARROW: 126,
39
+ };
40
+
41
+ const BUTTONS = {
42
+ left: {
43
+ cgButton: "$.kCGMouseButtonLeft",
44
+ down: "$.kCGEventLeftMouseDown",
45
+ up: "$.kCGEventLeftMouseUp",
46
+ },
47
+ right: {
48
+ cgButton: "$.kCGMouseButtonRight",
49
+ down: "$.kCGEventRightMouseDown",
50
+ up: "$.kCGEventRightMouseUp",
51
+ },
52
+ middle: {
53
+ cgButton: "$.kCGMouseButtonCenter",
54
+ down: "$.kCGEventOtherMouseDown",
55
+ up: "$.kCGEventOtherMouseUp",
56
+ },
57
+ };
58
+
59
+ function sleep(ms) {
60
+ return new Promise((resolve) => setTimeout(resolve, ms));
61
+ }
62
+
63
+ function shellEscapeSingleQuotes(value) {
64
+ return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
65
+ }
66
+
67
+ function normalizeKey(key) {
68
+ return String(key || "")
69
+ .trim()
70
+ .toUpperCase()
71
+ .replace(/[\s-]+/g, "")
72
+ .replace(/_/g, "");
73
+ }
74
+
75
+ function sanitizeCoordinate(value, fallback = 0) {
76
+ const num = Number(value);
77
+ if (!Number.isFinite(num)) return fallback;
78
+ return Math.max(0, Math.round(num));
79
+ }
80
+
81
+ function sanitizeButton(button) {
82
+ const raw = String(button || "left").toLowerCase();
83
+ if (raw === "center") return "middle";
84
+ return BUTTONS[raw] ? raw : "left";
85
+ }
86
+
87
+ function mapModifier(key) {
88
+ if (key === "CMDORCTRL") return "command down";
89
+ if (key === "CMD" || key === "COMMAND" || key === "META") return "command down";
90
+ if (key === "CTRL" || key === "CONTROL") return "control down";
91
+ if (key === "OPT") return "option down";
92
+ if (key === "ALT" || key === "OPTION") return "option down";
93
+ if (key === "SUPER" || key === "WINDOWS") return "command down";
94
+ if (key === "SHIFT") return "shift down";
95
+ return null;
96
+ }
97
+
98
+ function keyNameToCharacter(key) {
99
+ if (key === "SPACE") return " ";
100
+ if (key.length === 1) return key.toLowerCase();
101
+ return null;
102
+ }
103
+
104
+ async function runAppleScript(script) {
105
+ try {
106
+ await execFileAsync("osascript", ["-e", script]);
107
+ } catch (error) {
108
+ const reason = error instanceof Error ? error.message : String(error);
109
+ throw new Error(`AppleScript execution failed on macOS: ${reason}`);
110
+ }
111
+ }
112
+
113
+ async function runJxa(script) {
114
+ const args = ["-l", "JavaScript", "-e", script];
115
+ let lastError = null;
116
+ for (let attempt = 0; attempt < 3; attempt += 1) {
117
+ try {
118
+ await execFileAsync("osascript", args);
119
+ return;
120
+ } catch (error) {
121
+ lastError = error;
122
+ await sleep(80 * (attempt + 1));
123
+ }
124
+ }
125
+
126
+ const scriptFile = path.join(
127
+ os.tmpdir(),
128
+ `opencore-jxa-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
129
+ );
130
+ try {
131
+ await fs.writeFile(scriptFile, `${script}\n`, "utf8");
132
+ await execFileAsync("osascript", ["-l", "JavaScript", scriptFile]);
133
+ } catch (error) {
134
+ const failed = error || lastError;
135
+ const message = failed instanceof Error ? failed.message : String(failed);
136
+ const stderr = String(failed?.stderr || "").trim();
137
+ const detail = stderr ? `${message} | stderr: ${stderr}` : message;
138
+ throw new Error(
139
+ "JXA execution failed on macOS. " +
140
+ "Ensure Accessibility and Input Monitoring are enabled for your terminal app in System Settings > Privacy & Security. " +
141
+ `Details: ${detail}`,
142
+ );
143
+ } finally {
144
+ fs.unlink(scriptFile).catch(() => {});
145
+ }
146
+ }
147
+
148
+ async function downscaleImageToLimit(sourceFile, targetBytes = API_IMAGE_TARGET_BYTES) {
149
+ const maxDims = [1800, 1600, 1400, 1200, 1024, 900, 768, 640];
150
+ const tempFiles = [];
151
+ try {
152
+ for (const dim of maxDims) {
153
+ const outFile = path.join(
154
+ os.tmpdir(),
155
+ `computer-use-scaled-${Date.now()}-${dim}-${Math.random().toString(36).slice(2)}.png`,
156
+ );
157
+ tempFiles.push(outFile);
158
+ await execFileAsync("sips", [sourceFile, "--resampleHeightWidthMax", String(dim), "--out", outFile]);
159
+ const scaled = await fs.readFile(outFile);
160
+ if (scaled.byteLength <= targetBytes) {
161
+ return scaled;
162
+ }
163
+ }
164
+ return null;
165
+ } finally {
166
+ for (const file of tempFiles) {
167
+ fs.unlink(file).catch(() => {});
168
+ }
169
+ }
170
+ }
171
+
172
+ async function convertToJpegToLimit(sourceFile, targetBytes = API_IMAGE_TARGET_BYTES) {
173
+ const maxDims = [1600, 1400, 1200, 1024, 900, 768, 640];
174
+ const qualities = [80, 70, 60, 50, 40];
175
+ const tempFiles = [];
176
+ try {
177
+ for (const dim of maxDims) {
178
+ for (const quality of qualities) {
179
+ const outFile = path.join(
180
+ os.tmpdir(),
181
+ `computer-use-jpeg-${Date.now()}-${dim}-${quality}-${Math.random().toString(36).slice(2)}.jpg`,
182
+ );
183
+ tempFiles.push(outFile);
184
+ await execFileAsync("sips", [
185
+ sourceFile,
186
+ "--resampleHeightWidthMax",
187
+ String(dim),
188
+ "--setProperty",
189
+ "format",
190
+ "jpeg",
191
+ "--setProperty",
192
+ "formatOptions",
193
+ String(quality),
194
+ "--out",
195
+ outFile,
196
+ ]);
197
+ const jpeg = await fs.readFile(outFile);
198
+ if (jpeg.byteLength <= targetBytes) {
199
+ return jpeg;
200
+ }
201
+ }
202
+ }
203
+ return null;
204
+ } finally {
205
+ for (const file of tempFiles) {
206
+ fs.unlink(file).catch(() => {});
207
+ }
208
+ }
209
+ }
210
+
211
+ export class MacController {
212
+ constructor({ displayWidth, displayHeight, screenshotDelayMs = 300 }) {
213
+ this.displayWidth = Number(displayWidth) || null;
214
+ this.displayHeight = Number(displayHeight) || null;
215
+ this.screenshotDelayMs = screenshotDelayMs;
216
+ this.cursorX = 0;
217
+ this.cursorY = 0;
218
+ this.lastScreenshotBase64 = null;
219
+ this.lastScreenshotBytes = 0;
220
+ this.lastScreenshotMediaType = "image/png";
221
+ }
222
+
223
+ async init() {
224
+ if (this.displayWidth && this.displayHeight) return;
225
+
226
+ const fallback = { width: 1440, height: 900 };
227
+ try {
228
+ const script = [
229
+ 'ObjC.import("CoreGraphics");',
230
+ "const main = $.CGMainDisplayID();",
231
+ "console.log(`${$.CGDisplayPixelsWide(main)} ${$.CGDisplayPixelsHigh(main)}`);",
232
+ ].join(" ");
233
+ const { stdout } = await execFileAsync("osascript", ["-l", "JavaScript", "-e", script]);
234
+ const [w, h] = stdout.trim().split(/\s+/).map(Number);
235
+ this.displayWidth = Number.isFinite(w) && w > 0 ? w : fallback.width;
236
+ this.displayHeight = Number.isFinite(h) && h > 0 ? h : fallback.height;
237
+ } catch {
238
+ this.displayWidth = fallback.width;
239
+ this.displayHeight = fallback.height;
240
+ }
241
+ this.cursorX = Math.floor(this.displayWidth / 2);
242
+ this.cursorY = Math.floor(this.displayHeight / 2);
243
+ }
244
+
245
+ getToolConfig(environment = "mac") {
246
+ return {
247
+ type: "computer_use_preview",
248
+ display_width: this.displayWidth,
249
+ display_height: this.displayHeight,
250
+ environment,
251
+ };
252
+ }
253
+
254
+ async captureScreenshotBase64() {
255
+ const file = path.join(
256
+ os.tmpdir(),
257
+ `computer-use-${Date.now()}-${Math.random().toString(36).slice(2)}.png`,
258
+ );
259
+ try {
260
+ let captureError = null;
261
+ const attempts = [
262
+ ["-x", file],
263
+ ["-x", "-D", "1", file],
264
+ ];
265
+ for (const args of attempts) {
266
+ try {
267
+ await execFileAsync("screencapture", args);
268
+ captureError = null;
269
+ break;
270
+ } catch (error) {
271
+ captureError = error;
272
+ await sleep(120);
273
+ }
274
+ }
275
+ if (captureError) throw captureError;
276
+ let data = await fs.readFile(file);
277
+ let mediaType = "image/png";
278
+ if (data.byteLength > API_IMAGE_MAX_BYTES) {
279
+ const scaled = await downscaleImageToLimit(file);
280
+ if (scaled) data = scaled;
281
+ }
282
+ if (data.byteLength > API_IMAGE_MAX_BYTES) {
283
+ const jpeg = await convertToJpegToLimit(file);
284
+ if (jpeg) {
285
+ data = jpeg;
286
+ mediaType = "image/jpeg";
287
+ }
288
+ }
289
+ if (data.byteLength > API_IMAGE_MAX_BYTES) {
290
+ throw new Error(
291
+ `Screenshot too large for API after resize attempts (${data.byteLength} bytes > ${API_IMAGE_MAX_BYTES} bytes).`,
292
+ );
293
+ }
294
+ const encoded = data.toString("base64");
295
+ this.lastScreenshotBase64 = encoded;
296
+ this.lastScreenshotBytes = data.byteLength;
297
+ this.lastScreenshotMediaType = mediaType;
298
+ return encoded;
299
+ } catch (error) {
300
+ if (this.lastScreenshotBase64 && this.lastScreenshotBytes > 0 && this.lastScreenshotBytes <= API_IMAGE_MAX_BYTES) {
301
+ return this.lastScreenshotBase64;
302
+ }
303
+ const reason = error instanceof Error ? error.message : String(error);
304
+ throw new Error(
305
+ "Screenshot capture failed on macOS. " +
306
+ "Allow Screen Recording for your terminal app in System Settings > Privacy & Security > Screen Recording, " +
307
+ "then fully quit and reopen the terminal. Also ensure a display is active/unlocked. " +
308
+ `Original error: ${reason}`,
309
+ );
310
+ } finally {
311
+ fs.unlink(file).catch(() => {});
312
+ }
313
+ }
314
+
315
+ getLastScreenshotMediaType() {
316
+ return this.lastScreenshotMediaType || "image/png";
317
+ }
318
+
319
+ async executeAction(action = {}) {
320
+ const type = String(action.type || "").toLowerCase();
321
+
322
+ switch (type) {
323
+ case "click":
324
+ return this.click(action);
325
+ case "double_click":
326
+ return this.doubleClick(action);
327
+ case "move":
328
+ return this.move(action);
329
+ case "drag":
330
+ return this.drag(action);
331
+ case "scroll":
332
+ return this.scroll(action);
333
+ case "type":
334
+ return this.type(action);
335
+ case "keypress":
336
+ return this.keypress(action);
337
+ case "wait":
338
+ return sleep(Math.max(200, Number(action.ms || action.seconds * 1000 || 1000)));
339
+ case "screenshot":
340
+ return;
341
+ default:
342
+ throw new Error(`Unsupported action type: ${action.type ?? "unknown"}`);
343
+ }
344
+ }
345
+
346
+ async executeAnthropicAction(input = {}) {
347
+ const action = String(input.action || "").toLowerCase();
348
+ const [cx, cy] = Array.isArray(input.coordinate) ? input.coordinate : [null, null];
349
+ const withCoord = (base = {}) => ({ ...base, x: cx, y: cy });
350
+
351
+ switch (action) {
352
+ case "screenshot":
353
+ case "cursor_position":
354
+ return;
355
+ case "mouse_move":
356
+ return this.move(withCoord());
357
+ case "left_click":
358
+ return this.click(withCoord({ button: "left" }));
359
+ case "right_click":
360
+ return this.click(withCoord({ button: "right" }));
361
+ case "middle_click":
362
+ return this.click(withCoord({ button: "middle" }));
363
+ case "double_click":
364
+ return this.doubleClick(withCoord({ button: "left" }));
365
+ case "triple_click":
366
+ await this.doubleClick(withCoord({ button: "left" }));
367
+ return this.click(withCoord({ button: "left" }));
368
+ case "left_mouse_down":
369
+ return this.mouseDown(withCoord({ button: "left" }));
370
+ case "left_mouse_up":
371
+ return this.mouseUp(withCoord({ button: "left" }));
372
+ case "left_click_drag":
373
+ return this.dragAnthropic(input);
374
+ case "scroll":
375
+ return this.scrollAnthropic(input);
376
+ case "key":
377
+ return this.keyAnthropic(input);
378
+ case "hold_key":
379
+ return this.holdKeyAnthropic(input);
380
+ case "type":
381
+ return this.type({ text: input.text });
382
+ case "wait":
383
+ return sleep(Math.max(200, Number((input.duration_seconds || 1) * 1000)));
384
+ default:
385
+ throw new Error(`Unsupported anthropic action: ${input.action ?? "unknown"}`);
386
+ }
387
+ }
388
+
389
+ async move({ x, y }) {
390
+ const px = sanitizeCoordinate(x);
391
+ const py = sanitizeCoordinate(y);
392
+ const script = [
393
+ 'ObjC.import("CoreGraphics");',
394
+ `var point = $.CGPointMake(${px}, ${py});`,
395
+ "var ev = $.CGEventCreateMouseEvent(null, $.kCGEventMouseMoved, point, $.kCGMouseButtonLeft);",
396
+ "$.CGEventPost($.kCGHIDEventTap, ev);",
397
+ "$.CFRelease(ev);",
398
+ ].join(" ");
399
+ await runJxa(script);
400
+ this.cursorX = px;
401
+ this.cursorY = py;
402
+ }
403
+
404
+ async click({ x, y, button }) {
405
+ const px = sanitizeCoordinate(x, this.cursorX);
406
+ const py = sanitizeCoordinate(y, this.cursorY);
407
+ const choice = BUTTONS[sanitizeButton(button)];
408
+ const script = [
409
+ 'ObjC.import("CoreGraphics");',
410
+ `var point = $.CGPointMake(${px}, ${py});`,
411
+ `var down = $.CGEventCreateMouseEvent(null, ${choice.down}, point, ${choice.cgButton});`,
412
+ `var up = $.CGEventCreateMouseEvent(null, ${choice.up}, point, ${choice.cgButton});`,
413
+ "$.CGEventPost($.kCGHIDEventTap, down);",
414
+ "$.CGEventPost($.kCGHIDEventTap, up);",
415
+ "$.CFRelease(down);",
416
+ "$.CFRelease(up);",
417
+ ].join(" ");
418
+ await runJxa(script);
419
+ this.cursorX = px;
420
+ this.cursorY = py;
421
+ }
422
+
423
+ async doubleClick(action) {
424
+ await this.click(action);
425
+ await sleep(70);
426
+ await this.click(action);
427
+ }
428
+
429
+ async drag(action) {
430
+ const points = Array.isArray(action.path) ? action.path : [];
431
+ if (!points.length) {
432
+ const x = sanitizeCoordinate(action.x);
433
+ const y = sanitizeCoordinate(action.y);
434
+ const x2 = sanitizeCoordinate(action.x2 ?? action.to_x ?? x);
435
+ const y2 = sanitizeCoordinate(action.y2 ?? action.to_y ?? y);
436
+ points.push({ x, y }, { x: x2, y: y2 });
437
+ }
438
+ const safePoints = points
439
+ .map((point) => ({
440
+ x: sanitizeCoordinate(point.x),
441
+ y: sanitizeCoordinate(point.y),
442
+ }))
443
+ .filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
444
+ if (safePoints.length < 2) return;
445
+
446
+ const start = safePoints[0];
447
+ const scriptLines = [
448
+ 'ObjC.import("CoreGraphics");',
449
+ "function post(type, x, y) {",
450
+ " var point = $.CGPointMake(x, y);",
451
+ " var ev = $.CGEventCreateMouseEvent(null, type, point, $.kCGMouseButtonLeft);",
452
+ " $.CGEventPost($.kCGHIDEventTap, ev);",
453
+ " $.CFRelease(ev);",
454
+ "}",
455
+ `post($.kCGEventMouseMoved, ${start.x}, ${start.y});`,
456
+ `post($.kCGEventLeftMouseDown, ${start.x}, ${start.y});`,
457
+ ];
458
+
459
+ for (const point of safePoints.slice(1)) {
460
+ scriptLines.push(`post($.kCGEventLeftMouseDragged, ${point.x}, ${point.y});`);
461
+ }
462
+
463
+ const end = safePoints[safePoints.length - 1];
464
+ scriptLines.push(`post($.kCGEventLeftMouseUp, ${end.x}, ${end.y});`);
465
+ await runJxa(scriptLines.join(" "));
466
+ this.cursorX = end.x;
467
+ this.cursorY = end.y;
468
+ }
469
+
470
+ async scroll(action) {
471
+ const x = sanitizeCoordinate(action.x, Math.floor(this.displayWidth / 2));
472
+ const y = sanitizeCoordinate(action.y, Math.floor(this.displayHeight / 2));
473
+ const horizontal = Math.round(Number(action.scroll_x || 0));
474
+ const vertical = -Math.round(Number(action.scroll_y || 0));
475
+ const script = [
476
+ 'ObjC.import("CoreGraphics");',
477
+ `var point = $.CGPointMake(${x}, ${y});`,
478
+ "var move = $.CGEventCreateMouseEvent(null, $.kCGEventMouseMoved, point, $.kCGMouseButtonLeft);",
479
+ "$.CGEventPost($.kCGHIDEventTap, move);",
480
+ "$.CFRelease(move);",
481
+ `var ev = $.CGEventCreateScrollWheelEvent(null, $.kCGScrollEventUnitLine, 2, ${vertical}, ${horizontal});`,
482
+ "$.CGEventPost($.kCGHIDEventTap, ev);",
483
+ "$.CFRelease(ev);",
484
+ ].join(" ");
485
+ await runJxa(script);
486
+ }
487
+
488
+ async dragAnthropic(input) {
489
+ const [toX, toY] = Array.isArray(input.coordinate) ? input.coordinate : [this.cursorX, this.cursorY];
490
+ const [fromX, fromY] = Array.isArray(input.from_coordinate)
491
+ ? input.from_coordinate
492
+ : [this.cursorX, this.cursorY];
493
+ return this.drag({
494
+ path: [
495
+ { x: fromX, y: fromY },
496
+ { x: toX, y: toY },
497
+ ],
498
+ });
499
+ }
500
+
501
+ async scrollAnthropic(input) {
502
+ const amount = Math.round(Number(input.scroll_amount || input.amount || 0));
503
+ const direction = String(input.scroll_direction || input.direction || "down").toLowerCase();
504
+ const modifiers = Array.isArray(input.modifiers) ? input.modifiers : [];
505
+ const payload = { x: this.cursorX, y: this.cursorY, scroll_x: 0, scroll_y: 0 };
506
+
507
+ if (direction === "up") payload.scroll_y = -amount;
508
+ else if (direction === "left") payload.scroll_x = -amount;
509
+ else if (direction === "right") payload.scroll_x = amount;
510
+ else payload.scroll_y = amount;
511
+
512
+ if (modifiers.length) {
513
+ await this.keypress({ keys: modifiers.map(String) });
514
+ }
515
+ await this.scroll(payload);
516
+ }
517
+
518
+ async keyAnthropic(input) {
519
+ const combo = String(input.text || input.key || "").trim();
520
+ if (!combo) return;
521
+ const keys = combo
522
+ .split("+")
523
+ .map((part) => part.trim())
524
+ .filter(Boolean);
525
+ return this.keypress({ keys });
526
+ }
527
+
528
+ async holdKeyAnthropic(input) {
529
+ await this.keyAnthropic(input);
530
+ return sleep(Math.max(200, Number((input.duration_seconds || 1) * 1000)));
531
+ }
532
+
533
+ async mouseDown({ x, y, button }) {
534
+ const px = sanitizeCoordinate(x, this.cursorX);
535
+ const py = sanitizeCoordinate(y, this.cursorY);
536
+ const choice = BUTTONS[sanitizeButton(button)];
537
+ const script = [
538
+ 'ObjC.import("CoreGraphics");',
539
+ `var point = $.CGPointMake(${px}, ${py});`,
540
+ `var down = $.CGEventCreateMouseEvent(null, ${choice.down}, point, ${choice.cgButton});`,
541
+ "$.CGEventPost($.kCGHIDEventTap, down);",
542
+ "$.CFRelease(down);",
543
+ ].join(" ");
544
+ await runJxa(script);
545
+ this.cursorX = px;
546
+ this.cursorY = py;
547
+ }
548
+
549
+ async mouseUp({ x, y, button }) {
550
+ const px = sanitizeCoordinate(x, this.cursorX);
551
+ const py = sanitizeCoordinate(y, this.cursorY);
552
+ const choice = BUTTONS[sanitizeButton(button)];
553
+ const script = [
554
+ 'ObjC.import("CoreGraphics");',
555
+ `var point = $.CGPointMake(${px}, ${py});`,
556
+ `var up = $.CGEventCreateMouseEvent(null, ${choice.up}, point, ${choice.cgButton});`,
557
+ "$.CGEventPost($.kCGHIDEventTap, up);",
558
+ "$.CFRelease(up);",
559
+ ].join(" ");
560
+ await runJxa(script);
561
+ this.cursorX = px;
562
+ this.cursorY = py;
563
+ }
564
+
565
+ getCursorPosition() {
566
+ return { x: this.cursorX, y: this.cursorY };
567
+ }
568
+
569
+ async type({ text }) {
570
+ const value = String(text ?? "");
571
+ if (!value.length) return;
572
+ const lines = value.split("\n");
573
+ for (let i = 0; i < lines.length; i += 1) {
574
+ const chunk = lines[i];
575
+ if (chunk.length) {
576
+ const safe = shellEscapeSingleQuotes(chunk);
577
+ await runAppleScript(`tell application "System Events" to keystroke "${safe}"`);
578
+ }
579
+ if (i < lines.length - 1) {
580
+ await runAppleScript('tell application "System Events" to key code 36');
581
+ }
582
+ }
583
+ }
584
+
585
+ async keypress({ keys }) {
586
+ const list = Array.isArray(keys) ? keys : [];
587
+ if (!list.length) return;
588
+
589
+ const normalized = list.map(normalizeKey).filter(Boolean);
590
+ const modifiers = normalized.map(mapModifier).filter(Boolean);
591
+ const nonModifierKeys = normalized.filter((key) => !mapModifier(key));
592
+ const main = nonModifierKeys[0];
593
+
594
+ if (!main && modifiers.length) return;
595
+ if (!main) return;
596
+
597
+ const usingClause = modifiers.length ? ` using {${modifiers.join(", ")}}` : "";
598
+ const keyCode = SPECIAL_KEY_CODES[main];
599
+
600
+ if (typeof keyCode === "number") {
601
+ await runAppleScript(`tell application "System Events" to key code ${keyCode}${usingClause}`);
602
+ return;
603
+ }
604
+
605
+ const char = keyNameToCharacter(main);
606
+ if (char !== null) {
607
+ const safe = shellEscapeSingleQuotes(char);
608
+ await runAppleScript(`tell application "System Events" to keystroke "${safe}"${usingClause}`);
609
+ return;
610
+ }
611
+
612
+ throw new Error(`Unsupported key in keypress action: ${main}`);
613
+ }
614
+ }