bingocode 1.0.40 → 1.1.42

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 (63) hide show
  1. package/bin/bingo-win.cjs +2 -1
  2. package/bin/bingocode-win.cjs +2 -1
  3. package/bin/claude-win.cjs +2 -1
  4. package/bun.lock +1716 -0
  5. package/package.json +14 -2
  6. package/src/server/config/providers.yaml +1 -1
  7. package/src/server/proxy/transform/anthropicToOpenaiChat.ts +23 -9
  8. package/adapters/README.md +0 -87
  9. package/adapters/common/__tests__/chat-queue.test.ts +0 -61
  10. package/adapters/common/__tests__/format.test.ts +0 -148
  11. package/adapters/common/__tests__/http-client.test.ts +0 -105
  12. package/adapters/common/__tests__/message-buffer.test.ts +0 -84
  13. package/adapters/common/__tests__/message-dedup.test.ts +0 -57
  14. package/adapters/common/__tests__/session-store.test.ts +0 -62
  15. package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
  16. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
  17. package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
  18. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
  19. package/adapters/common/attachment/attachment-limits.ts +0 -58
  20. package/adapters/common/attachment/attachment-store.ts +0 -121
  21. package/adapters/common/attachment/attachment-types.ts +0 -29
  22. package/adapters/common/attachment/image-block-watcher.ts +0 -94
  23. package/adapters/common/chat-queue.ts +0 -24
  24. package/adapters/common/config.ts +0 -96
  25. package/adapters/common/format.ts +0 -229
  26. package/adapters/common/http-client.ts +0 -107
  27. package/adapters/common/message-buffer.ts +0 -91
  28. package/adapters/common/message-dedup.ts +0 -57
  29. package/adapters/common/pairing.ts +0 -149
  30. package/adapters/common/session-store.ts +0 -60
  31. package/adapters/common/ws-bridge.ts +0 -282
  32. package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
  33. package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
  34. package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
  35. package/adapters/feishu/__tests__/feishu.test.ts +0 -907
  36. package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
  37. package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
  38. package/adapters/feishu/__tests__/media.test.ts +0 -120
  39. package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
  40. package/adapters/feishu/card-errors.ts +0 -151
  41. package/adapters/feishu/cardkit.ts +0 -294
  42. package/adapters/feishu/extract-payload.ts +0 -95
  43. package/adapters/feishu/flush-controller.ts +0 -149
  44. package/adapters/feishu/index.ts +0 -1275
  45. package/adapters/feishu/markdown-style.ts +0 -212
  46. package/adapters/feishu/media.ts +0 -176
  47. package/adapters/feishu/streaming-card.ts +0 -612
  48. package/adapters/package.json +0 -23
  49. package/adapters/telegram/__tests__/media.test.ts +0 -86
  50. package/adapters/telegram/__tests__/telegram.test.ts +0 -115
  51. package/adapters/telegram/index.ts +0 -754
  52. package/adapters/telegram/media.ts +0 -89
  53. package/adapters/tsconfig.json +0 -18
  54. package/runtime/mac_helper.py +0 -775
  55. package/runtime/requirements-win.txt +0 -7
  56. package/runtime/requirements.txt +0 -6
  57. package/runtime/test_helpers.py +0 -322
  58. package/runtime/win_helper.py +0 -723
  59. package/scripts/count-app-loc.ts +0 -256
  60. package/scripts/release.ts +0 -130
  61. package/start-cli.bat +0 -7
  62. package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
  63. package/stubs/color-diff-napi.ts +0 -45
@@ -1,775 +0,0 @@
1
- #!/usr/bin/env python3
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import base64
6
- import ctypes
7
- import json
8
- import os
9
- import subprocess
10
- import sys
11
- import time
12
- from io import BytesIO
13
- from pathlib import Path
14
- from typing import Any
15
-
16
- import mss
17
- from AppKit import NSWorkspace, NSPasteboard, NSPasteboardTypeString, NSURL
18
- from PIL import Image
19
- from Quartz import (
20
- CGDisplayBounds,
21
- CGDisplayIsMain,
22
- CGDisplayModeGetPixelHeight,
23
- CGDisplayModeGetPixelWidth,
24
- CGDisplayPixelsHigh,
25
- CGDisplayPixelsWide,
26
- CGGetActiveDisplayList,
27
- CGMainDisplayID,
28
- CGWindowListCopyWindowInfo,
29
- CGRectContainsPoint,
30
- CGRectIntersection,
31
- CGPointMake,
32
- CGPreflightScreenCaptureAccess,
33
- kCGNullWindowID,
34
- kCGWindowBounds,
35
- kCGWindowIsOnscreen,
36
- kCGWindowLayer,
37
- kCGWindowListExcludeDesktopElements,
38
- kCGWindowListOptionOnScreenOnly,
39
- kCGWindowName,
40
- kCGWindowOwnerName,
41
- )
42
-
43
- os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
44
- os.environ.setdefault("PYAUTOGUI_HIDE_SUPPORT_PROMPT", "1")
45
-
46
- import pyautogui # noqa: E402
47
-
48
- pyautogui.FAILSAFE = False
49
- pyautogui.PAUSE = 0
50
-
51
- KEY_MAP = {
52
- "a": "a",
53
- "b": "b",
54
- "c": "c",
55
- "d": "d",
56
- "e": "e",
57
- "f": "f",
58
- "g": "g",
59
- "h": "h",
60
- "i": "i",
61
- "j": "j",
62
- "k": "k",
63
- "l": "l",
64
- "m": "m",
65
- "n": "n",
66
- "o": "o",
67
- "p": "p",
68
- "q": "q",
69
- "r": "r",
70
- "s": "s",
71
- "t": "t",
72
- "u": "u",
73
- "v": "v",
74
- "w": "w",
75
- "x": "x",
76
- "y": "y",
77
- "z": "z",
78
- "0": "0",
79
- "1": "1",
80
- "2": "2",
81
- "3": "3",
82
- "4": "4",
83
- "5": "5",
84
- "6": "6",
85
- "7": "7",
86
- "8": "8",
87
- "9": "9",
88
- "cmd": "command",
89
- "command": "command",
90
- "meta": "command",
91
- "super": "command",
92
- "ctrl": "ctrl",
93
- "control": "ctrl",
94
- "shift": "shift",
95
- "alt": "option",
96
- "option": "option",
97
- "opt": "option",
98
- "fn": "fn",
99
- "escape": "esc",
100
- "esc": "esc",
101
- "enter": "enter",
102
- "return": "enter",
103
- "tab": "tab",
104
- "space": "space",
105
- "backspace": "backspace",
106
- "delete": "delete",
107
- "forwarddelete": "delete",
108
- "up": "up",
109
- "down": "down",
110
- "left": "left",
111
- "right": "right",
112
- "home": "home",
113
- "end": "end",
114
- "pageup": "pageup",
115
- "pagedown": "pagedown",
116
- "capslock": "capslock",
117
- "f1": "f1",
118
- "f2": "f2",
119
- "f3": "f3",
120
- "f4": "f4",
121
- "f5": "f5",
122
- "f6": "f6",
123
- "f7": "f7",
124
- "f8": "f8",
125
- "f9": "f9",
126
- "f10": "f10",
127
- "f11": "f11",
128
- "f12": "f12",
129
- "-": "minus",
130
- "=": "equals",
131
- "[": "[",
132
- "]": "]",
133
- "\\": "\\",
134
- ";": ";",
135
- "'": "'",
136
- ",": ",",
137
- ".": ".",
138
- "/": "/",
139
- "`": "`",
140
- }
141
-
142
-
143
- def normalize_key(name: str) -> str:
144
- key = name.strip().lower()
145
- if key not in KEY_MAP:
146
- raise ValueError(f"Unsupported key: {name}")
147
- return KEY_MAP[key]
148
-
149
-
150
- def json_output(payload: dict[str, Any]) -> None:
151
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
152
- sys.stdout.write("\n")
153
- sys.stdout.flush()
154
-
155
-
156
- def error_output(message: str, code: str = "runtime_error") -> None:
157
- json_output({"ok": False, "error": {"code": code, "message": message}})
158
-
159
-
160
- def bool_env(name: str, default: bool = False) -> bool:
161
- value = os.environ.get(name)
162
- if value is None:
163
- return default
164
- return value not in {"0", "false", "False", ""}
165
-
166
-
167
- def run_osascript(script: str) -> str:
168
- result = subprocess.run(
169
- ["osascript", "-e", script],
170
- text=True,
171
- capture_output=True,
172
- check=False,
173
- )
174
- if result.returncode != 0:
175
- raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "osascript failed")
176
- return result.stdout.strip()
177
-
178
-
179
- def applescript_modifier(name: str) -> str:
180
- if name == "command":
181
- return "command down"
182
- if name == "option":
183
- return "option down"
184
- if name == "shift":
185
- return "shift down"
186
- if name == "ctrl":
187
- return "control down"
188
- if name == "fn":
189
- return "fn down"
190
- raise ValueError(f"Unsupported AppleScript modifier: {name}")
191
-
192
-
193
- def send_keystroke_via_osascript(character: str, modifiers: list[str] | None = None) -> None:
194
- escaped = character.replace("\\", "\\\\").replace('"', '\\"')
195
- if modifiers:
196
- modifier_expr = ", ".join(applescript_modifier(m) for m in modifiers)
197
- script = (
198
- 'tell application "System Events" to keystroke '
199
- f'"{escaped}" using {{{modifier_expr}}}'
200
- )
201
- else:
202
- script = f'tell application "System Events" to keystroke "{escaped}"'
203
- run_osascript(script)
204
-
205
-
206
- def get_displays() -> list[dict[str, Any]]:
207
- max_displays = 32
208
- err, active, count = CGGetActiveDisplayList(max_displays, None, None)
209
- if err != 0:
210
- raise RuntimeError(f"CGGetActiveDisplayList failed: {err}")
211
- displays: list[dict[str, Any]] = []
212
- main_id = CGMainDisplayID()
213
- for idx, display_id in enumerate(active[:count]):
214
- bounds = CGDisplayBounds(display_id)
215
- mode = None
216
- try:
217
- from Quartz import CGDisplayCopyDisplayMode
218
- mode = CGDisplayCopyDisplayMode(display_id)
219
- except Exception:
220
- mode = None
221
- physical_width = int(CGDisplayPixelsWide(display_id))
222
- physical_height = int(CGDisplayPixelsHigh(display_id))
223
- logical_width = int(bounds.size.width)
224
- logical_height = int(bounds.size.height)
225
- if mode is not None:
226
- mode_w = int(CGDisplayModeGetPixelWidth(mode))
227
- mode_h = int(CGDisplayModeGetPixelHeight(mode))
228
- physical_width = mode_w or physical_width
229
- physical_height = mode_h or physical_height
230
- scale_factor = physical_width / logical_width if logical_width else 1
231
- name = f"Display {idx + 1}"
232
- displays.append(
233
- {
234
- "id": int(display_id),
235
- "displayId": int(display_id),
236
- "width": logical_width,
237
- "height": logical_height,
238
- "scaleFactor": scale_factor,
239
- "originX": int(bounds.origin.x),
240
- "originY": int(bounds.origin.y),
241
- "isPrimary": bool(display_id == main_id or CGDisplayIsMain(display_id)),
242
- "name": name,
243
- "label": name,
244
- }
245
- )
246
- return displays
247
-
248
-
249
- def choose_display(display_id: int | None) -> dict[str, Any]:
250
- displays = get_displays()
251
- if not displays:
252
- raise RuntimeError("No active displays found")
253
- if display_id is None:
254
- for display in displays:
255
- if display["isPrimary"]:
256
- return display
257
- return displays[0]
258
- for display in displays:
259
- if display["displayId"] == display_id or display["id"] == display_id:
260
- return display
261
- raise RuntimeError(f"Unknown display: {display_id}")
262
-
263
-
264
- def ensure_screen_recording_permission() -> None:
265
- """No-op: CGPreflightScreenCaptureAccess is unreliable for child processes
266
- (returns False even when the parent app has TCC permission), and any actual
267
- capture attempt triggers a macOS popup on newer versions. Let the actual
268
- capture call handle errors instead."""
269
- pass
270
-
271
-
272
- def capture_display(display_id: int | None, resize: tuple[int, int] | None = None) -> dict[str, Any]:
273
- ensure_screen_recording_permission()
274
- display = choose_display(display_id)
275
- monitor = {
276
- "left": display["originX"],
277
- "top": display["originY"],
278
- "width": display["width"],
279
- "height": display["height"],
280
- }
281
- with mss.mss() as sct:
282
- raw = sct.grab(monitor)
283
- image = Image.frombytes("RGB", raw.size, raw.rgb)
284
- if resize:
285
- image = image.resize(resize, Image.Resampling.LANCZOS)
286
- buffer = BytesIO()
287
- image.save(buffer, format="JPEG", quality=75, optimize=True)
288
- base64_data = base64.b64encode(buffer.getvalue()).decode("ascii")
289
- return {
290
- "base64": base64_data,
291
- "width": image.width,
292
- "height": image.height,
293
- "displayWidth": display["width"],
294
- "displayHeight": display["height"],
295
- "displayId": display["displayId"],
296
- "originX": display["originX"],
297
- "originY": display["originY"],
298
- "display": display,
299
- }
300
-
301
-
302
- def capture_region(region: dict[str, int], resize: tuple[int, int] | None = None) -> dict[str, Any]:
303
- ensure_screen_recording_permission()
304
- with mss.mss() as sct:
305
- raw = sct.grab(region)
306
- image = Image.frombytes("RGB", raw.size, raw.rgb)
307
- if resize:
308
- image = image.resize(resize, Image.Resampling.LANCZOS)
309
- buffer = BytesIO()
310
- image.save(buffer, format="JPEG", quality=75, optimize=True)
311
- base64_data = base64.b64encode(buffer.getvalue()).decode("ascii")
312
- return {"base64": base64_data, "width": image.width, "height": image.height}
313
-
314
-
315
- def list_windows() -> list[dict[str, Any]]:
316
- windows = CGWindowListCopyWindowInfo(
317
- kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
318
- kCGNullWindowID,
319
- )
320
- out: list[dict[str, Any]] = []
321
- for window in windows or []:
322
- if int(window.get(kCGWindowLayer, 0)) != 0:
323
- continue
324
- if not bool(window.get(kCGWindowIsOnscreen, True)):
325
- continue
326
- bounds = window.get(kCGWindowBounds) or {}
327
- width = int(bounds.get("Width", 0))
328
- height = int(bounds.get("Height", 0))
329
- if width <= 1 or height <= 1:
330
- continue
331
- out.append(
332
- {
333
- "ownerName": window.get(kCGWindowOwnerName, "") or "",
334
- "title": window.get(kCGWindowName, "") or "",
335
- "bounds": {
336
- "x": int(bounds.get("X", 0)),
337
- "y": int(bounds.get("Y", 0)),
338
- "width": width,
339
- "height": height,
340
- },
341
- }
342
- )
343
- return out
344
-
345
-
346
- def bundle_id_to_app(bundle_id: str):
347
- return NSWorkspace.sharedWorkspace().URLForApplicationWithBundleIdentifier_(bundle_id)
348
-
349
-
350
- def installed_apps() -> list[dict[str, Any]]:
351
- search_roots = [
352
- Path("/Applications"),
353
- Path.home() / "Applications",
354
- Path("/System/Applications"),
355
- Path("/System/Applications/Utilities"),
356
- ]
357
- results: dict[str, dict[str, Any]] = {}
358
- workspace = NSWorkspace.sharedWorkspace()
359
- for root in search_roots:
360
- if not root.exists():
361
- continue
362
- for app in root.rglob("*.app"):
363
- try:
364
- bundle = workspace.bundleIdentifierForURL_(NSURL.fileURLWithPath_(str(app)))
365
- except Exception:
366
- bundle = None
367
- if not bundle:
368
- try:
369
- url = workspace.URLForApplicationWithBundleIdentifier_(str(app))
370
- bundle = workspace.bundleIdentifierForURL_(url) if url else None
371
- except Exception:
372
- bundle = None
373
- info_plist = app / "Contents/Info.plist"
374
- display_name = app.stem
375
- if info_plist.exists():
376
- try:
377
- import plistlib
378
- with info_plist.open("rb") as f:
379
- plist = plistlib.load(f)
380
- bundle = bundle or plist.get("CFBundleIdentifier")
381
- display_name = plist.get("CFBundleDisplayName") or plist.get("CFBundleName") or display_name
382
- except Exception:
383
- pass
384
- if not bundle or bundle in results:
385
- continue
386
- results[bundle] = {
387
- "bundleId": str(bundle),
388
- "displayName": str(display_name),
389
- "path": str(app),
390
- }
391
- return sorted(results.values(), key=lambda item: item["displayName"].lower())
392
-
393
-
394
- def running_apps() -> list[dict[str, Any]]:
395
- apps = []
396
- seen = set()
397
- for app in NSWorkspace.sharedWorkspace().runningApplications() or []:
398
- bundle_id = app.bundleIdentifier()
399
- if not bundle_id or bundle_id in seen:
400
- continue
401
- seen.add(bundle_id)
402
- name = app.localizedName() or bundle_id
403
- apps.append({"bundleId": str(bundle_id), "displayName": str(name)})
404
- return sorted(apps, key=lambda item: item["displayName"].lower())
405
-
406
-
407
- def app_display_name(bundle_id: str) -> str | None:
408
- for app in NSWorkspace.sharedWorkspace().runningApplications() or []:
409
- if app.bundleIdentifier() == bundle_id:
410
- return str(app.localizedName() or bundle_id)
411
- for app in installed_apps():
412
- if app["bundleId"] == bundle_id:
413
- return str(app["displayName"])
414
- return None
415
-
416
-
417
- def frontmost_app() -> dict[str, str] | None:
418
- app = NSWorkspace.sharedWorkspace().frontmostApplication()
419
- if not app:
420
- return None
421
- bundle_id = app.bundleIdentifier()
422
- if not bundle_id:
423
- return None
424
- return {
425
- "bundleId": str(bundle_id),
426
- "displayName": str(app.localizedName() or bundle_id),
427
- }
428
-
429
-
430
- def app_under_point(x: int, y: int) -> dict[str, str] | None:
431
- point = CGPointMake(x, y)
432
- running_by_name = {
433
- str(app.localizedName() or app.bundleIdentifier()): str(app.bundleIdentifier())
434
- for app in NSWorkspace.sharedWorkspace().runningApplications() or []
435
- if app.bundleIdentifier()
436
- }
437
- for window in list_windows():
438
- bounds = window["bounds"]
439
- rect = ((bounds["x"], bounds["y"]), (bounds["width"], bounds["height"]))
440
- if CGRectContainsPoint(rect, point):
441
- owner = window["ownerName"]
442
- bundle = running_by_name.get(owner)
443
- if bundle:
444
- return {"bundleId": bundle, "displayName": str(owner)}
445
- return frontmost_app()
446
-
447
-
448
- def find_window_displays(bundle_ids: list[str]) -> list[dict[str, Any]]:
449
- if not bundle_ids:
450
- return []
451
- displays = get_displays()
452
- names_by_bundle = {
453
- bundle_id: app_display_name(bundle_id) or bundle_id for bundle_id in bundle_ids
454
- }
455
- windows = list_windows()
456
- result = []
457
- for bundle_id in bundle_ids:
458
- target_name = names_by_bundle.get(bundle_id)
459
- display_ids: set[int] = set()
460
- for window in windows:
461
- owner = window["ownerName"]
462
- if not owner:
463
- continue
464
- if target_name and owner != target_name:
465
- continue
466
- if not target_name and owner != bundle_id:
467
- continue
468
- wx = window["bounds"]["x"]
469
- wy = window["bounds"]["y"]
470
- ww = window["bounds"]["width"]
471
- wh = window["bounds"]["height"]
472
- window_rect = ((wx, wy), (ww, wh))
473
- for display in displays:
474
- display_rect = ((display["originX"], display["originY"]), (display["width"], display["height"]))
475
- intersection = CGRectIntersection(window_rect, display_rect)
476
- if intersection.size.width > 0 and intersection.size.height > 0:
477
- display_ids.add(int(display["displayId"]))
478
- result.append({"bundleId": bundle_id, "displayIds": sorted(display_ids)})
479
- return result
480
-
481
-
482
- def open_app(bundle_id: str) -> None:
483
- url = bundle_id_to_app(bundle_id)
484
- if not url:
485
- raise RuntimeError(f"App not found for bundle identifier: {bundle_id}")
486
- ok, err = NSWorkspace.sharedWorkspace().launchApplicationAtURL_options_configuration_error_(url, 0, {}, None)
487
- if not ok:
488
- raise RuntimeError(str(err) if err else f"Failed to open app {bundle_id}")
489
-
490
-
491
- def read_clipboard() -> str:
492
- pb = NSPasteboard.generalPasteboard()
493
- value = pb.stringForType_(NSPasteboardTypeString)
494
- return "" if value is None else str(value)
495
-
496
-
497
- def write_clipboard(text: str) -> None:
498
- pb = NSPasteboard.generalPasteboard()
499
- pb.clearContents()
500
- pb.setString_forType_(text, NSPasteboardTypeString)
501
-
502
-
503
- def paste_clipboard() -> None:
504
- send_keystroke_via_osascript("v", ["command"])
505
-
506
-
507
- def detect_screen_recording_permission() -> bool | None:
508
- """Best-effort passive screen-recording probe with no system prompt.
509
-
510
- `CGPreflightScreenCaptureAccess()` is fast and explicit when it returns
511
- True, but on child processes launched by a TCC-authorized app bundle it can
512
- still return False. As a fallback, inspect the visible window list: Apple
513
- only exposes other apps' window titles when Screen Recording access is
514
- granted. If we can see at least one title, treat the permission as granted.
515
- If we can inspect visible windows but every title is blank, treat it as not
516
- granted. If window enumeration itself is unavailable, return None.
517
- """
518
-
519
- try:
520
- if CGPreflightScreenCaptureAccess():
521
- return True
522
- except Exception:
523
- pass
524
-
525
- try:
526
- windows = CGWindowListCopyWindowInfo(
527
- kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
528
- kCGNullWindowID,
529
- )
530
- except Exception:
531
- return None
532
-
533
- eligible_windows = 0
534
- for window in windows or []:
535
- if int(window.get(kCGWindowLayer, 0)) != 0:
536
- continue
537
- if not bool(window.get(kCGWindowIsOnscreen, True)):
538
- continue
539
-
540
- bounds = window.get(kCGWindowBounds) or {}
541
- width = int(bounds.get("Width", 0))
542
- height = int(bounds.get("Height", 0))
543
- if width <= 1 or height <= 1:
544
- continue
545
-
546
- eligible_windows += 1
547
- if (window.get(kCGWindowName, "") or "").strip():
548
- return True
549
-
550
- if eligible_windows > 0:
551
- return False
552
- return None
553
-
554
-
555
- def detect_accessibility_permission() -> bool:
556
- """
557
- Use the official macOS Accessibility trust API.
558
-
559
- The previous System Events / AppleScript probe was too weak: it could
560
- succeed even when the current helper process was not actually trusted for
561
- input control, which led the desktop UI to report Accessibility as granted
562
- while mouse/keyboard control still failed at runtime.
563
- """
564
- framework_path = "/System/Library/Frameworks/ApplicationServices.framework/ApplicationServices"
565
- try:
566
- application_services = ctypes.CDLL(framework_path)
567
- application_services.AXIsProcessTrusted.restype = ctypes.c_bool
568
- application_services.AXIsProcessTrusted.argtypes = []
569
- return bool(application_services.AXIsProcessTrusted())
570
- except Exception:
571
- # Fail closed: if the trust API can't be queried, treat accessibility
572
- # as unavailable instead of reporting a misleading success state.
573
- return False
574
-
575
-
576
- def check_permissions() -> dict[str, bool | None]:
577
- accessibility = detect_accessibility_permission()
578
- screen_recording = detect_screen_recording_permission()
579
- return {
580
- "accessibility": accessibility,
581
- "screenRecording": screen_recording,
582
- }
583
-
584
-
585
- def click(x: int, y: int, button: str, count: int, modifiers: list[str] | None) -> None:
586
- pyautogui.moveTo(x, y)
587
- if modifiers:
588
- normalized = [normalize_key(m) for m in modifiers]
589
- for key in normalized:
590
- pyautogui.keyDown(key)
591
- try:
592
- pyautogui.click(x=x, y=y, button=button, clicks=count, interval=0.08)
593
- finally:
594
- for key in reversed(normalized):
595
- pyautogui.keyUp(key)
596
- else:
597
- pyautogui.click(x=x, y=y, button=button, clicks=count, interval=0.08)
598
-
599
-
600
- def scroll(x: int, y: int, delta_x: int, delta_y: int) -> None:
601
- pyautogui.moveTo(x, y)
602
- if delta_y:
603
- pyautogui.scroll(int(delta_y), x=x, y=y)
604
- if delta_x:
605
- pyautogui.hscroll(int(delta_x), x=x, y=y)
606
-
607
-
608
- def key_action(sequence: str, repeat: int = 1) -> None:
609
- parts = [normalize_key(part) for part in sequence.split("+") if part.strip()]
610
- for _ in range(max(1, repeat)):
611
- if parts == ["command", "v"]:
612
- paste_clipboard()
613
- elif parts == ["command", "a"]:
614
- send_keystroke_via_osascript("a", ["command"])
615
- elif parts == ["command", "c"]:
616
- send_keystroke_via_osascript("c", ["command"])
617
- elif parts == ["command", "x"]:
618
- send_keystroke_via_osascript("x", ["command"])
619
- elif len(parts) == 1:
620
- pyautogui.press(parts[0])
621
- else:
622
- pyautogui.hotkey(*parts, interval=0.02)
623
- time.sleep(0.01)
624
-
625
-
626
- def hold_keys(keys: list[str], duration_ms: int) -> None:
627
- normalized = [normalize_key(k) for k in keys]
628
- for key in normalized:
629
- pyautogui.keyDown(key)
630
- try:
631
- time.sleep(max(duration_ms, 0) / 1000)
632
- finally:
633
- for key in reversed(normalized):
634
- pyautogui.keyUp(key)
635
-
636
-
637
- def type_text(text: str) -> None:
638
- pyautogui.write(text, interval=0.008)
639
-
640
-
641
- def main() -> int:
642
- parser = argparse.ArgumentParser()
643
- parser.add_argument("command")
644
- parser.add_argument("--payload", default="{}")
645
- args = parser.parse_args()
646
- payload = json.loads(args.payload)
647
-
648
- try:
649
- command = args.command
650
- if command == "check_permissions":
651
- perms = check_permissions()
652
- json_output({"ok": True, "result": perms})
653
- return 0
654
- if command == "list_displays":
655
- json_output({"ok": True, "result": get_displays()})
656
- return 0
657
- if command == "get_display_size":
658
- json_output({"ok": True, "result": choose_display(payload.get("displayId"))})
659
- return 0
660
- if command == "screenshot":
661
- resize = None
662
- if payload.get("targetWidth") and payload.get("targetHeight"):
663
- resize = (int(payload["targetWidth"]), int(payload["targetHeight"]))
664
- result = capture_display(payload.get("displayId"), resize)
665
- json_output({"ok": True, "result": result})
666
- return 0
667
- if command == "resolve_prepare_capture":
668
- resize = None
669
- if payload.get("targetWidth") and payload.get("targetHeight"):
670
- resize = (int(payload["targetWidth"]), int(payload["targetHeight"]))
671
- result = capture_display(payload.get("preferredDisplayId"), resize)
672
- result["hidden"] = []
673
- result["resolvedDisplayId"] = result["displayId"]
674
- json_output({"ok": True, "result": result})
675
- return 0
676
- if command == "zoom":
677
- resize = None
678
- if payload.get("targetWidth") and payload.get("targetHeight"):
679
- resize = (int(payload["targetWidth"]), int(payload["targetHeight"]))
680
- region = {
681
- "left": int(payload["x"]),
682
- "top": int(payload["y"]),
683
- "width": int(payload["width"]),
684
- "height": int(payload["height"]),
685
- }
686
- json_output({"ok": True, "result": capture_region(region, resize)})
687
- return 0
688
- if command == "prepare_for_action":
689
- json_output({"ok": True, "result": []})
690
- return 0
691
- if command == "preview_hide_set":
692
- json_output({"ok": True, "result": []})
693
- return 0
694
- if command == "find_window_displays":
695
- json_output({"ok": True, "result": find_window_displays(list(payload.get("bundleIds") or []))})
696
- return 0
697
- if command == "key":
698
- key_action(str(payload["keySequence"]), int(payload.get("repeat") or 1))
699
- json_output({"ok": True, "result": True})
700
- return 0
701
- if command == "hold_key":
702
- hold_keys(list(payload.get("keyNames") or []), int(payload.get("durationMs") or 0))
703
- json_output({"ok": True, "result": True})
704
- return 0
705
- if command == "type":
706
- type_text(str(payload.get("text") or ""))
707
- json_output({"ok": True, "result": True})
708
- return 0
709
- if command == "click":
710
- click(int(payload["x"]), int(payload["y"]), str(payload.get("button") or "left"), int(payload.get("count") or 1), payload.get("modifiers"))
711
- json_output({"ok": True, "result": True})
712
- return 0
713
- if command == "drag":
714
- from_point = payload.get("from")
715
- if from_point:
716
- pyautogui.moveTo(int(from_point["x"]), int(from_point["y"]))
717
- pyautogui.dragTo(int(payload["to"]["x"]), int(payload["to"]["y"]), duration=0.2, button="left")
718
- json_output({"ok": True, "result": True})
719
- return 0
720
- if command == "move_mouse":
721
- pyautogui.moveTo(int(payload["x"]), int(payload["y"]))
722
- json_output({"ok": True, "result": True})
723
- return 0
724
- if command == "scroll":
725
- scroll(int(payload["x"]), int(payload["y"]), int(payload.get("deltaX") or 0), int(payload.get("deltaY") or 0))
726
- json_output({"ok": True, "result": True})
727
- return 0
728
- if command == "mouse_down":
729
- pyautogui.mouseDown(button="left")
730
- json_output({"ok": True, "result": True})
731
- return 0
732
- if command == "mouse_up":
733
- pyautogui.mouseUp(button="left")
734
- json_output({"ok": True, "result": True})
735
- return 0
736
- if command == "cursor_position":
737
- x, y = pyautogui.position()
738
- json_output({"ok": True, "result": {"x": int(x), "y": int(y)}})
739
- return 0
740
- if command == "frontmost_app":
741
- json_output({"ok": True, "result": frontmost_app()})
742
- return 0
743
- if command == "app_under_point":
744
- json_output({"ok": True, "result": app_under_point(int(payload["x"]), int(payload["y"]))})
745
- return 0
746
- if command == "list_installed_apps":
747
- json_output({"ok": True, "result": installed_apps()})
748
- return 0
749
- if command == "list_running_apps":
750
- json_output({"ok": True, "result": running_apps()})
751
- return 0
752
- if command == "open_app":
753
- open_app(str(payload["bundleId"]))
754
- json_output({"ok": True, "result": True})
755
- return 0
756
- if command == "read_clipboard":
757
- json_output({"ok": True, "result": read_clipboard()})
758
- return 0
759
- if command == "write_clipboard":
760
- write_clipboard(str(payload.get("text") or ""))
761
- json_output({"ok": True, "result": True})
762
- return 0
763
- if command == "paste_clipboard":
764
- paste_clipboard()
765
- json_output({"ok": True, "result": True})
766
- return 0
767
- error_output(f"Unknown command: {command}", code="bad_command")
768
- return 2
769
- except Exception as exc:
770
- error_output(str(exc))
771
- return 1
772
-
773
-
774
- if __name__ == "__main__":
775
- raise SystemExit(main())