@beeos-ai/device-mcp-server 0.2.3

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 (62) hide show
  1. package/LICENSE +201 -0
  2. package/dist/backends/android-adb-runner.d.ts +32 -0
  3. package/dist/backends/android-adb-runner.js +15 -0
  4. package/dist/backends/android-adb-runner.js.map +1 -0
  5. package/dist/backends/android-adb.d.ts +153 -0
  6. package/dist/backends/android-adb.js +723 -0
  7. package/dist/backends/android-adb.js.map +1 -0
  8. package/dist/backends/base.d.ts +150 -0
  9. package/dist/backends/base.js +116 -0
  10. package/dist/backends/base.js.map +1 -0
  11. package/dist/backends/desktop.d.ts +62 -0
  12. package/dist/backends/desktop.js +176 -0
  13. package/dist/backends/desktop.js.map +1 -0
  14. package/dist/backends/index.d.ts +63 -0
  15. package/dist/backends/index.js +105 -0
  16. package/dist/backends/index.js.map +1 -0
  17. package/dist/backends/linux.d.ts +69 -0
  18. package/dist/backends/linux.js +230 -0
  19. package/dist/backends/linux.js.map +1 -0
  20. package/dist/backends/mac.d.ts +154 -0
  21. package/dist/backends/mac.js +881 -0
  22. package/dist/backends/mac.js.map +1 -0
  23. package/dist/backends/stubs/ios.d.ts +17 -0
  24. package/dist/backends/stubs/ios.js +32 -0
  25. package/dist/backends/stubs/ios.js.map +1 -0
  26. package/dist/backends/stubs/macos.d.ts +13 -0
  27. package/dist/backends/stubs/macos.js +27 -0
  28. package/dist/backends/stubs/macos.js.map +1 -0
  29. package/dist/backends/stubs/windows.d.ts +69 -0
  30. package/dist/backends/stubs/windows.js +191 -0
  31. package/dist/backends/stubs/windows.js.map +1 -0
  32. package/dist/cli.d.ts +37 -0
  33. package/dist/cli.js +177 -0
  34. package/dist/cli.js.map +1 -0
  35. package/dist/index.d.ts +13 -0
  36. package/dist/index.js +14 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/server/action-mapping.d.ts +21 -0
  39. package/dist/server/action-mapping.js +153 -0
  40. package/dist/server/action-mapping.js.map +1 -0
  41. package/dist/server/app.d.ts +23 -0
  42. package/dist/server/app.js +157 -0
  43. package/dist/server/app.js.map +1 -0
  44. package/dist/server/tool-registry.d.ts +50 -0
  45. package/dist/server/tool-registry.js +504 -0
  46. package/dist/server/tool-registry.js.map +1 -0
  47. package/dist/util/adb-files.d.ts +92 -0
  48. package/dist/util/adb-files.js +221 -0
  49. package/dist/util/adb-files.js.map +1 -0
  50. package/dist/util/adb-shell.d.ts +80 -0
  51. package/dist/util/adb-shell.js +102 -0
  52. package/dist/util/adb-shell.js.map +1 -0
  53. package/dist/util/android-apps.d.ts +10 -0
  54. package/dist/util/android-apps.js +103 -0
  55. package/dist/util/android-apps.js.map +1 -0
  56. package/dist/util/image-dim.d.ts +27 -0
  57. package/dist/util/image-dim.js +37 -0
  58. package/dist/util/image-dim.js.map +1 -0
  59. package/dist/util/ui-xml.d.ts +20 -0
  60. package/dist/util/ui-xml.js +184 -0
  61. package/dist/util/ui-xml.js.map +1 -0
  62. package/package.json +56 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Backend registry — string → backend factory.
3
+ *
4
+ * Used by the CLI (`device-mcp-server --backend android ...`) and
5
+ * `server/app.ts` to resolve a `SandboxBackend` from configuration.
6
+ *
7
+ * **Naming convention** (after the self-hosted plan §3.4 "去掉 backend
8
+ * 私有词"):
9
+ *
10
+ * Canonical short name (CLI surface) Aliases (back-compat / verbose)
11
+ * --------------------------------- --------------------------------------
12
+ * adb → AndroidAdbBackend "android"
13
+ * macos → MacOsBackend "mac", "desktop-macos"
14
+ * linux → LinuxBackend "desktop-linux" (used to be the mock —
15
+ * now resolves to the real native
16
+ * LinuxBackend; the in-memory mock is
17
+ * still reachable via "mock-desktop"
18
+ * and is used by integration tests)
19
+ * windows → WindowsBackend "win", "desktop-windows"
20
+ * ios → IosBackend (zero caps stub, reserved for WDA / libimobiledevice)
21
+ *
22
+ * desktop "smart-resolve": MacOsBackend on darwin,
23
+ * LinuxBackend on linux, WindowsBackend
24
+ * on win32, mock elsewhere.
25
+ *
26
+ * mock-desktop DesktopBackend (in-memory, for tests
27
+ * — never picks itself in `desktop`).
28
+ */
29
+ import { DeviceError } from "@beeos-ai/device-common";
30
+ import { AndroidAdbBackend } from "./android-adb.js";
31
+ import { DesktopBackend } from "./desktop.js";
32
+ import { LinuxBackend } from "./linux.js";
33
+ import { MacOsBackend } from "./mac.js";
34
+ import { WindowsBackend } from "./stubs/windows.js";
35
+ import { IosBackend } from "./stubs/ios.js";
36
+ /**
37
+ * Resolve any of the canonical / alias / smart-resolve names to a real
38
+ * backend instance. Aliases collapse early so the rest of the codebase
39
+ * only ever sees the canonical name when building options.
40
+ */
41
+ export function createBackend(name, opts = {}) {
42
+ switch (name) {
43
+ /* — Android / ADB — */
44
+ case "adb":
45
+ case "android":
46
+ return new AndroidAdbBackend(opts.android);
47
+ /* — macOS — */
48
+ case "macos":
49
+ case "mac":
50
+ case "desktop-macos":
51
+ return new MacOsBackend(opts.macos);
52
+ /* — Linux (real native, gnome-screenshot/scrot/import + /bin/sh) — */
53
+ case "linux":
54
+ case "desktop-linux":
55
+ return new LinuxBackend(opts.linux);
56
+ /* — Windows (real native, PowerShell + cmd.exe) — */
57
+ case "windows":
58
+ case "win":
59
+ case "desktop-windows":
60
+ return new WindowsBackend(opts.windows);
61
+ /* — Smart-resolve — */
62
+ case "desktop":
63
+ if (process.platform === "darwin")
64
+ return new MacOsBackend(opts.macos);
65
+ if (process.platform === "linux")
66
+ return new LinuxBackend(opts.linux);
67
+ if (process.platform === "win32")
68
+ return new WindowsBackend(opts.windows);
69
+ // Anything exotic falls back to the mock so the agent main loop
70
+ // still boots in unusual sandboxes / CI runners.
71
+ return new DesktopBackend({ ...opts.desktop, os: "desktop-linux" });
72
+ /* — Explicit in-memory mock for tests — */
73
+ case "mock-desktop":
74
+ return new DesktopBackend({ ...opts.desktop, os: "desktop-linux" });
75
+ /* — Reserved stub — */
76
+ case "ios":
77
+ return new IosBackend();
78
+ default:
79
+ throw new DeviceError(`unknown backend '${name}'`, {
80
+ subtype: "unsupported",
81
+ retriable: false,
82
+ });
83
+ }
84
+ }
85
+ /**
86
+ * `--backend` autocomplete / `--help` source. Lists canonical names
87
+ * first, then aliases, so help output reads top-down most-useful.
88
+ */
89
+ export const SUPPORTED_BACKENDS = [
90
+ "adb",
91
+ "macos",
92
+ "linux",
93
+ "windows",
94
+ "desktop",
95
+ "android",
96
+ "mac",
97
+ "win",
98
+ "desktop-linux",
99
+ "desktop-windows",
100
+ "desktop-macos",
101
+ "mock-desktop",
102
+ "ios",
103
+ ];
104
+ export { AndroidAdbBackend, DesktopBackend, LinuxBackend, MacOsBackend, WindowsBackend, IosBackend, };
105
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backends/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAAE,iBAAiB,EAAiC,MAAM,kBAAkB,CAAC;AACpF,OAAO,EAAE,cAAc,EAA8B,MAAM,cAAc,CAAC;AAC1E,OAAO,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAC;AACpE,OAAO,EAAE,YAAY,EAA4B,MAAM,UAAU,CAAC;AAClE,OAAO,EAAE,cAAc,EAA8B,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAsC5C;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAiB,EACjB,OAA6B,EAAE;IAE/B,QAAQ,IAAI,EAAE,CAAC;QACb,uBAAuB;QACvB,KAAK,KAAK,CAAC;QACX,KAAK,SAAS;YACZ,OAAO,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAE7C,eAAe;QACf,KAAK,OAAO,CAAC;QACb,KAAK,KAAK,CAAC;QACX,KAAK,eAAe;YAClB,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAEtC,sEAAsE;QACtE,KAAK,OAAO,CAAC;QACb,KAAK,eAAe;YAClB,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAEtC,qDAAqD;QACrD,KAAK,SAAS,CAAC;QACf,KAAK,KAAK,CAAC;QACX,KAAK,iBAAiB;YACpB,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAE1C,uBAAuB;QACvB,KAAK,SAAS;YACZ,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;gBAAE,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvE,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;gBAAE,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtE,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;gBAAE,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAC1E,gEAAgE;YAChE,iDAAiD;YACjD,OAAO,IAAI,cAAc,CAAC,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;QAEtE,2CAA2C;QAC3C,KAAK,cAAc;YACjB,OAAO,IAAI,cAAc,CAAC,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,CAAC,CAAC;QAEtE,uBAAuB;QACvB,KAAK,KAAK;YACR,OAAO,IAAI,UAAU,EAAE,CAAC;QAE1B;YACE,MAAM,IAAI,WAAW,CAAC,oBAAoB,IAAc,GAAG,EAAE;gBAC3D,OAAO,EAAE,aAAa;gBACtB,SAAS,EAAE,KAAK;aACjB,CAAC,CAAC;IACP,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAkB;IAC/C,KAAK;IACL,OAAO;IACP,OAAO;IACP,SAAS;IACT,SAAS;IACT,SAAS;IACT,KAAK;IACL,KAAK;IACL,eAAe;IACf,iBAAiB;IACjB,eAAe;IACf,cAAc;IACd,KAAK;CACN,CAAC;AAYF,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,UAAU,GACX,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * LinuxBackend — minimal `SandboxBackend` driving a real Linux host.
3
+ *
4
+ * Scope (v0.2.0 stub): only `screenshot` / `screen_size` / `device_info` /
5
+ * `execute_command` are implemented natively. Pointer / keyboard /
6
+ * accessibility tools are deliberately NOT in `LINUX_STUB_TOOLS` so the MCP
7
+ * tool registry filters them out at startup — agents see exactly the verbs
8
+ * the backend can actually fulfill, no runtime "unsupported" surprises.
9
+ *
10
+ * Implementations (all synchronous shell-outs to keep deps zero):
11
+ *
12
+ * - **Screenshot**: prefer `gnome-screenshot --file -` if available
13
+ * (GNOME / Wayland-native), then `scrot -o -` (X11), then ImageMagick
14
+ * `import -window root png:-`. The first one that exists on `PATH`
15
+ * wins. All three pipe PNG bytes to stdout, so the implementation
16
+ * stays uniform — we just buffer the child's stdout.
17
+ * - **Screen size**: parsed from `xdpyinfo | grep dimensions:` on X11,
18
+ * falls back to `wlr-randr` on Wayland-Sway, finally falls back to
19
+ * `1920x1080` if neither is available so the agent main loop still
20
+ * boots on headless CI containers (the screenshot call will then
21
+ * surface a `DeviceError("environment")` which is the right place to
22
+ * report the missing tooling).
23
+ * - **executeCommand**: `/bin/sh -c body` — same shape as ADB / macOS.
24
+ *
25
+ * **Testability**: like `MacOsBackend`, the constructor accepts an
26
+ * injected `runner` (spawn-shape) so the suite can run without ever
27
+ * touching a real X server / Wayland compositor. The default runner
28
+ * binds to `node:child_process.spawn`.
29
+ */
30
+ import { type DeviceInfo } from "@beeos-ai/device-common";
31
+ import { BaseSandboxBackend, type ExecuteCommandOutput, type ScreenSize, type ScreenshotOutput } from "./base.js";
32
+ export interface RunResult {
33
+ code: number;
34
+ stdout: Buffer;
35
+ stderr: string;
36
+ }
37
+ export type Runner = (bin: string, args: readonly string[], opts?: {
38
+ stdin?: string;
39
+ timeoutMs?: number;
40
+ }) => Promise<RunResult>;
41
+ export interface LinuxBackendOptions {
42
+ /** Override the spawn runner (tests). */
43
+ runner?: Runner;
44
+ /**
45
+ * Override which-binary lookup. Defaults to a runner-based `command -v`
46
+ * probe; tests inject a `Set<string>` of "available" binaries.
47
+ */
48
+ whichBinary?: (bin: string) => Promise<boolean>;
49
+ /** Default-shell command timeout in ms. Defaults to 30s. */
50
+ shellTimeoutMs?: number;
51
+ }
52
+ export declare class LinuxBackend extends BaseSandboxBackend {
53
+ readonly os: "desktop-linux";
54
+ readonly tools: ReadonlySet<string>;
55
+ private readonly runner;
56
+ private readonly whichBinary;
57
+ private readonly shellTimeoutMs;
58
+ private screenshotBackend;
59
+ constructor(opts?: LinuxBackendOptions);
60
+ info(): Promise<DeviceInfo>;
61
+ screenshot(): Promise<ScreenshotOutput>;
62
+ screenSize(): Promise<ScreenSize>;
63
+ executeCommand(command: string): Promise<ExecuteCommandOutput>;
64
+ /**
65
+ * Pick whichever screenshot tool is on `PATH`, in priority order.
66
+ * Cached after the first probe.
67
+ */
68
+ private pickScreenshotTool;
69
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * LinuxBackend — minimal `SandboxBackend` driving a real Linux host.
3
+ *
4
+ * Scope (v0.2.0 stub): only `screenshot` / `screen_size` / `device_info` /
5
+ * `execute_command` are implemented natively. Pointer / keyboard /
6
+ * accessibility tools are deliberately NOT in `LINUX_STUB_TOOLS` so the MCP
7
+ * tool registry filters them out at startup — agents see exactly the verbs
8
+ * the backend can actually fulfill, no runtime "unsupported" surprises.
9
+ *
10
+ * Implementations (all synchronous shell-outs to keep deps zero):
11
+ *
12
+ * - **Screenshot**: prefer `gnome-screenshot --file -` if available
13
+ * (GNOME / Wayland-native), then `scrot -o -` (X11), then ImageMagick
14
+ * `import -window root png:-`. The first one that exists on `PATH`
15
+ * wins. All three pipe PNG bytes to stdout, so the implementation
16
+ * stays uniform — we just buffer the child's stdout.
17
+ * - **Screen size**: parsed from `xdpyinfo | grep dimensions:` on X11,
18
+ * falls back to `wlr-randr` on Wayland-Sway, finally falls back to
19
+ * `1920x1080` if neither is available so the agent main loop still
20
+ * boots on headless CI containers (the screenshot call will then
21
+ * surface a `DeviceError("environment")` which is the right place to
22
+ * report the missing tooling).
23
+ * - **executeCommand**: `/bin/sh -c body` — same shape as ADB / macOS.
24
+ *
25
+ * **Testability**: like `MacOsBackend`, the constructor accepts an
26
+ * injected `runner` (spawn-shape) so the suite can run without ever
27
+ * touching a real X server / Wayland compositor. The default runner
28
+ * binds to `node:child_process.spawn`.
29
+ */
30
+ import { spawn } from "node:child_process";
31
+ import { DeviceError, } from "@beeos-ai/device-common";
32
+ import { BaseSandboxBackend, } from "./base.js";
33
+ import { parsePngDimensions } from "../util/image-dim.js";
34
+ /**
35
+ * Linux backend tool catalogue (v0.2.0 stub).
36
+ *
37
+ * Only the four tools the stub can actually fulfill — observation +
38
+ * shell — are advertised. Everything else (gestures / keyboard / fs /
39
+ * install / ui_dump) is omitted so the tool registry never offers a
40
+ * verb the stub will reject.
41
+ *
42
+ * Future expansion path:
43
+ * - click / drag / scroll → `xdotool` (X11) or `ydotool` (Wayland)
44
+ * - type / key / hotkey → same
45
+ * - launch_app → `wmctrl` / `gtk-launch` / .desktop lookup
46
+ * - install_package → `apt-get` / `dnf` / `pacman` dispatch
47
+ * - file_read / write / list_directory → Node `fs/promises` direct
48
+ * - ui_dump → AT-SPI bridge (atspi-2 + dbus)
49
+ */
50
+ const LINUX_STUB_TOOLS = new Set([
51
+ "screenshot",
52
+ "screen_size",
53
+ "device_info",
54
+ "execute_command",
55
+ ]);
56
+ const defaultRunner = (bin, args, opts = {}) => new Promise((resolve, reject) => {
57
+ const child = spawn(bin, args, {
58
+ stdio: ["pipe", "pipe", "pipe"],
59
+ });
60
+ const out = [];
61
+ let err = "";
62
+ let timer;
63
+ if (opts.timeoutMs && opts.timeoutMs > 0) {
64
+ timer = setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs);
65
+ }
66
+ child.stdout.on("data", (b) => out.push(b));
67
+ child.stderr.on("data", (b) => (err += b.toString("utf8")));
68
+ child.on("error", (e) => {
69
+ if (timer)
70
+ clearTimeout(timer);
71
+ reject(e);
72
+ });
73
+ child.on("close", (code) => {
74
+ if (timer)
75
+ clearTimeout(timer);
76
+ resolve({ code: code ?? 0, stdout: Buffer.concat(out), stderr: err });
77
+ });
78
+ if (opts.stdin) {
79
+ child.stdin.end(opts.stdin);
80
+ }
81
+ else {
82
+ child.stdin.end();
83
+ }
84
+ });
85
+ /* ----------------------------------------------------------------------- */
86
+ /* LinuxBackend */
87
+ /* ----------------------------------------------------------------------- */
88
+ export class LinuxBackend extends BaseSandboxBackend {
89
+ os = "desktop-linux";
90
+ tools = LINUX_STUB_TOOLS;
91
+ runner;
92
+ whichBinary;
93
+ shellTimeoutMs;
94
+ // Cached at first use to avoid re-probing every screenshot.
95
+ screenshotBackend = null;
96
+ constructor(opts = {}) {
97
+ super();
98
+ this.runner = opts.runner ?? defaultRunner;
99
+ this.shellTimeoutMs = opts.shellTimeoutMs ?? 30_000;
100
+ this.whichBinary =
101
+ opts.whichBinary ??
102
+ (async (bin) => {
103
+ const r = await this.runner("/bin/sh", ["-c", `command -v ${bin}`]);
104
+ return r.code === 0 && r.stdout.length > 0;
105
+ });
106
+ }
107
+ async info() {
108
+ // `DeviceCapability` is the agent-facing verb vocabulary (see
109
+ // common/device-info.ts). `screenshot` + `shell` are the two verbs
110
+ // the v0.2.0 stub actually fulfills. Distinct from `LINUX_STUB_TOOLS`
111
+ // above — that one drives the tool registry.
112
+ const caps = ["screenshot", "shell"];
113
+ const size = await this.screenSize().catch(() => ({ w: 1920, h: 1080 }));
114
+ return {
115
+ type: this.os,
116
+ name: "Linux desktop (stub)",
117
+ os: "Linux",
118
+ width: size.w,
119
+ height: size.h,
120
+ capabilities: caps,
121
+ metadata: {
122
+ stub: "true",
123
+ screenshotBackend: this.screenshotBackend ?? "auto",
124
+ },
125
+ };
126
+ }
127
+ async screenshot() {
128
+ const tool = await this.pickScreenshotTool();
129
+ let result;
130
+ switch (tool) {
131
+ case "gnome-screenshot":
132
+ result = await this.runner("gnome-screenshot", ["--file=-"]);
133
+ break;
134
+ case "scrot":
135
+ result = await this.runner("scrot", ["-o", "-"]);
136
+ break;
137
+ case "import":
138
+ result = await this.runner("import", ["-window", "root", "png:-"]);
139
+ break;
140
+ }
141
+ if (result.code !== 0 || result.stdout.length === 0) {
142
+ throw new DeviceError(`linux screenshot via ${tool} failed: ${result.stderr.trim()}`, {
143
+ subtype: "environment",
144
+ retriable: true,
145
+ });
146
+ }
147
+ const png = result.stdout;
148
+ let width = 1920;
149
+ let height = 1080;
150
+ try {
151
+ const dims = parsePngDimensions(png);
152
+ width = dims[0];
153
+ height = dims[1];
154
+ }
155
+ catch {
156
+ /* fall back to size guess */
157
+ }
158
+ return { data: png, width, height, format: "png" };
159
+ }
160
+ async screenSize() {
161
+ // X11 first (xdpyinfo) — present on every desktop distro, doesn't
162
+ // need root, and prints `dimensions: WIDTHxHEIGHT pixels`.
163
+ const xdpy = await this.runner("/bin/sh", [
164
+ "-c",
165
+ "xdpyinfo 2>/dev/null | awk '/dimensions:/ {print $2; exit}'",
166
+ ]);
167
+ const xdpyOut = xdpy.stdout.toString("utf8").trim();
168
+ const xdpyMatch = xdpyOut.match(/^(\d+)x(\d+)$/);
169
+ if (xdpy.code === 0 && xdpyMatch) {
170
+ return { w: Number(xdpyMatch[1]), h: Number(xdpyMatch[2]) };
171
+ }
172
+ // Wayland (Sway / wlroots) — `wlr-randr` lists outputs; the first
173
+ // line containing "current" carries `WIDTHxHEIGHT@HZ`.
174
+ const wlr = await this.runner("/bin/sh", [
175
+ "-c",
176
+ "wlr-randr 2>/dev/null | awk '/current/ {gsub(/@.*/, \"\", $1); print $1; exit}'",
177
+ ]);
178
+ const wlrOut = wlr.stdout.toString("utf8").trim();
179
+ const wlrMatch = wlrOut.match(/^(\d+)x(\d+)$/);
180
+ if (wlr.code === 0 && wlrMatch) {
181
+ return { w: Number(wlrMatch[1]), h: Number(wlrMatch[2]) };
182
+ }
183
+ // Last-resort fallback. The screenshot path will surface a real
184
+ // error if we're actually running headless.
185
+ return { w: 1920, h: 1080 };
186
+ }
187
+ async executeCommand(command) {
188
+ const r = await this.runner("/bin/sh", ["-c", command], {
189
+ timeoutMs: this.shellTimeoutMs,
190
+ });
191
+ return {
192
+ stdout: r.stdout.toString("utf8"),
193
+ stderr: r.stderr,
194
+ exitCode: r.code,
195
+ };
196
+ }
197
+ /* All non-capability verbs (pointer / keyboard / navigation / app /
198
+ * filesystem / install / accessibility) inherit the
199
+ * `unsupported(verb)` rejection from `BaseSandboxBackend`. They are
200
+ * NEVER reached at runtime because the tool registry filters them
201
+ * out at `/mcp/tools/list` and `/mcp/tools/call` based on the
202
+ * `capabilities` set above — but defining the inherited stubs as
203
+ * the safety net keeps `instanceof SandboxBackend` clean for code
204
+ * paths that bypass the registry (e.g. legacy `/act` callers). */
205
+ /* --------------------------------------------------------------------- */
206
+ /* Helpers */
207
+ /* --------------------------------------------------------------------- */
208
+ /**
209
+ * Pick whichever screenshot tool is on `PATH`, in priority order.
210
+ * Cached after the first probe.
211
+ */
212
+ async pickScreenshotTool() {
213
+ if (this.screenshotBackend)
214
+ return this.screenshotBackend;
215
+ if (await this.whichBinary("gnome-screenshot")) {
216
+ this.screenshotBackend = "gnome-screenshot";
217
+ return "gnome-screenshot";
218
+ }
219
+ if (await this.whichBinary("scrot")) {
220
+ this.screenshotBackend = "scrot";
221
+ return "scrot";
222
+ }
223
+ if (await this.whichBinary("import")) {
224
+ this.screenshotBackend = "import";
225
+ return "import";
226
+ }
227
+ throw new DeviceError("linux screenshot requires `gnome-screenshot`, `scrot`, or ImageMagick `import` on PATH", { subtype: "environment", retriable: false });
228
+ }
229
+ }
230
+ //# sourceMappingURL=linux.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linux.js","sourceRoot":"","sources":["../../src/backends/linux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EACL,WAAW,GAGZ,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,kBAAkB,GAInB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAS;IAC5D,YAAY;IACZ,aAAa;IACb,aAAa;IACb,iBAAiB;CAClB,CAAC,CAAC;AAkBH,MAAM,aAAa,GAAW,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,EAAE,EAAE,CACrD,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IACzC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE;QAC7B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;KAChC,CAAC,CAAC;IACH,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,KAAiC,CAAC;IACtC,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC;QACzC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAClE,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACpE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;QACtB,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/B,MAAM,CAAC,CAAC,CAAC,CAAC;IACZ,CAAC,CAAC,CAAC;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;QACzB,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/B,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;IACH,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;IACpB,CAAC;AACH,CAAC,CAAC,CAAC;AAkBL,6EAA6E;AAC7E,8EAA8E;AAC9E,6EAA6E;AAE7E,MAAM,OAAO,YAAa,SAAQ,kBAAkB;IACzC,EAAE,GAAG,eAAwB,CAAC;IACrB,KAAK,GAAG,gBAAgB,CAAC;IAE1B,MAAM,CAAS;IACf,WAAW,CAAoC;IAC/C,cAAc,CAAS;IAExC,4DAA4D;IACpD,iBAAiB,GAAmD,IAAI,CAAC;IAEjF,YAAY,OAA4B,EAAE;QACxC,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,aAAa,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,cAAc,IAAI,MAAM,CAAC;QACpD,IAAI,CAAC,WAAW;YACd,IAAI,CAAC,WAAW;gBAChB,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;oBACb,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,cAAc,GAAG,EAAE,CAAC,CAAC,CAAC;oBACpE,OAAO,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;gBAC7C,CAAC,CAAC,CAAC;IACP,CAAC;IAED,KAAK,CAAC,IAAI;QACR,8DAA8D;QAC9D,mEAAmE;QACnE,sEAAsE;QACtE,6CAA6C;QAC7C,MAAM,IAAI,GAAuB,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACzD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACzE,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,EAAE;YACb,IAAI,EAAE,sBAAsB;YAC5B,EAAE,EAAE,OAAO;YACX,KAAK,EAAE,IAAI,CAAC,CAAC;YACb,MAAM,EAAE,IAAI,CAAC,CAAC;YACd,YAAY,EAAE,IAAI;YAClB,QAAQ,EAAE;gBACR,IAAI,EAAE,MAAM;gBACZ,iBAAiB,EAAE,IAAI,CAAC,iBAAiB,IAAI,MAAM;aACpD;SACF,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,kBAAkB,EAAE,CAAC;QAC7C,IAAI,MAAiB,CAAC;QACtB,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,kBAAkB;gBACrB,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;gBAC7D,MAAM;YACR,KAAK,OAAO;gBACV,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;gBACjD,MAAM;YACR,KAAK,QAAQ;gBACX,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;gBACnE,MAAM;QACV,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,WAAW,CAAC,wBAAwB,IAAI,YAAY,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE;gBACpF,OAAO,EAAE,aAAa;gBACtB,SAAS,EAAE,IAAI;aAChB,CAAC,CAAC;QACL,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;QAC1B,IAAI,KAAK,GAAG,IAAI,CAAC;QACjB,IAAI,MAAM,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;YACrC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YAChB,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACP,6BAA6B;QAC/B,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,UAAU;QACd,kEAAkE;QAClE,2DAA2D;QAC3D,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;YACxC,IAAI;YACJ,6DAA6D;SAC9D,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACpD,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QACjD,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,SAAS,EAAE,CAAC;YACjC,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,CAAC;QAED,kEAAkE;QAClE,uDAAuD;QACvD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;YACvC,IAAI;YACJ,iFAAiF;SAClF,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAC/C,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,CAAC;QAED,gEAAgE;QAChE,4CAA4C;QAC5C,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,OAAe;QAClC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE;YACtD,SAAS,EAAE,IAAI,CAAC,cAAc;SAC/B,CAAC,CAAC;QACH,OAAO;YACL,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YACjC,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,QAAQ,EAAE,CAAC,CAAC,IAAI;SACjB,CAAC;IACJ,CAAC;IAED;;;;;;;sEAOkE;IAElE,2EAA2E;IAC3E,4EAA4E;IAC5E,2EAA2E;IAE3E;;;OAGG;IACK,KAAK,CAAC,kBAAkB;QAG9B,IAAI,IAAI,CAAC,iBAAiB;YAAE,OAAO,IAAI,CAAC,iBAAiB,CAAC;QAC1D,IAAI,MAAM,IAAI,CAAC,WAAW,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAC/C,IAAI,CAAC,iBAAiB,GAAG,kBAAkB,CAAC;YAC5C,OAAO,kBAAkB,CAAC;QAC5B,CAAC;QACD,IAAI,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC,iBAAiB,GAAG,OAAO,CAAC;YACjC,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,IAAI,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,iBAAiB,GAAG,QAAQ,CAAC;YAClC,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,IAAI,WAAW,CACnB,wFAAwF,EACxF,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,EAAE,CAC7C,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * MacOsBackend — concrete `SandboxBackend` driving a real macOS host.
3
+ *
4
+ * Best-practice topology after researching the field (Anthropic Computer Use,
5
+ * OpenInterpreter os-mode, mac-control skill, the Python reference at
6
+ * `deviceagent_research/.../mcp_tools/backend_macos.py`, etc.):
7
+ *
8
+ * - **Pointer** (click / move / drag / scroll / double / right / longPress):
9
+ * `cliclick` (Homebrew, optional) preferred when on `PATH`; falls back to
10
+ * `osascript` System Events when absent. cliclick is significantly more
11
+ * reliable for coordinate-based mouse synthesis on modern (sandboxed) apps.
12
+ * - **Keyboard** (typeText / pressKey / hotkey): `osascript` System Events.
13
+ * Handles unicode, modifier chords, and raw key codes. No extra dependency.
14
+ * - **Screenshot**: `screencapture -x -t png <tmpfile>`, read back into
15
+ * memory, unlink — `screencapture` does NOT support stdout output despite
16
+ * `-o -` / `-` folklore, so we round-trip through a per-call temp file.
17
+ * `sharp` then resizes to the display's logical-point resolution so click
18
+ * coords and screenshot pixels share one coordinate system (Retina-correct).
19
+ * - **Screen size**: `system_profiler SPDisplaysDataType -json` parsed at
20
+ * `connect()` time, cached for the process lifetime.
21
+ * - **Launch app**: `open -a NAME` for human names, `open -b BUNDLE_ID` for
22
+ * reverse-DNS bundle ids, `open <path>` for `.app`/`.dmg`/`.pkg`.
23
+ * - **executeCommand**: `/bin/sh -c body`. Same shape as the Android backend.
24
+ * - **file_read / write / list_directory**: Node `fs/promises` direct (we
25
+ * run *on* the host).
26
+ * - **Mobile verb aliasing** (matches reference convention): `tap → click`,
27
+ * `swipe → drag`, `long_press → cliclick dd + sleep + du`. `back` is
28
+ * unsupported on macOS, `home` is mapped to F11 (Mission Control "show
29
+ * desktop").
30
+ *
31
+ * **Permissions**: the host process MUST hold Accessibility permission
32
+ * (System Settings → Privacy & Security → Accessibility). Without it macOS
33
+ * silently drops every synthetic input event. There is no programmatic
34
+ * bypass — we surface a `DeviceError("permission_required")` when cliclick /
35
+ * osascript exit non-zero so the operator can grant the permission.
36
+ *
37
+ * **Testability**: the constructor accepts an injected `runner` (an
38
+ * `execFile`-like async callable) and an `fs` shim. Defaults bind to a
39
+ * spawn-backed runner and `node:fs/promises`. Unit tests pass spy runners
40
+ * so the suite runs identically on Linux CI without any real shell calls.
41
+ */
42
+ import { type DeviceInfo } from "@beeos-ai/device-common";
43
+ import { BaseSandboxBackend, type ExecuteCommandOutput, type ListDirectoryEntry, type ScreenSize, type ScreenshotOutput } from "./base.js";
44
+ export interface MacRunResult {
45
+ stdout: string | Buffer;
46
+ stderr: string;
47
+ }
48
+ export interface MacRunOptions {
49
+ encoding?: "buffer" | "utf8";
50
+ timeoutMs?: number;
51
+ input?: string | Buffer;
52
+ cwd?: string;
53
+ }
54
+ /**
55
+ * Async `execFile`-shaped callable. Throws on non-zero exit (with `code`,
56
+ * `stdout`, `stderr` decorated on the rejected error) so the backend can
57
+ * recognise the standard `NodeJS.ErrnoException` shape.
58
+ */
59
+ export type MacRunner = (file: string, args: string[], opts?: MacRunOptions) => Promise<MacRunResult>;
60
+ export interface MacFsShim {
61
+ readFile: (path: string) => Promise<Buffer>;
62
+ writeFile: (path: string, data: Buffer | string) => Promise<void>;
63
+ readdir: (path: string, opts: {
64
+ withFileTypes: true;
65
+ }) => Promise<Array<{
66
+ name: string;
67
+ isDirectory(): boolean;
68
+ }>>;
69
+ stat: (path: string) => Promise<{
70
+ size: number;
71
+ isDirectory(): boolean;
72
+ }>;
73
+ access: (path: string) => Promise<void>;
74
+ unlink: (path: string) => Promise<void>;
75
+ tmpdir: () => string;
76
+ }
77
+ export interface MacOsBackendOptions {
78
+ /** Override the default `child_process.spawn`-backed runner (tests). */
79
+ runner?: MacRunner;
80
+ /** Override `node:fs/promises` (tests). */
81
+ fs?: MacFsShim;
82
+ /**
83
+ * Override the resolved cliclick binary path. When omitted we probe
84
+ * `command -v cliclick` once at `connect()` time. Tests typically set this
85
+ * to "cliclick" (presence) or omit it together with `forceFallback`.
86
+ */
87
+ cliclickPath?: string;
88
+ /**
89
+ * When true, behave as if cliclick is absent — every pointer verb takes the
90
+ * `osascript` fallback path. Useful for unit tests and for hosts where
91
+ * cliclick is not installed.
92
+ */
93
+ forceFallback?: boolean;
94
+ /** Override autodetected pixel-to-point ratio (tests, multi-display). */
95
+ retinaScale?: number;
96
+ /** Override autodetected logical screen size (tests). */
97
+ size?: ScreenSize;
98
+ /** Sleep helper, injectable for deterministic tests of `longPress`. */
99
+ sleep?: (ms: number) => Promise<void>;
100
+ }
101
+ export declare class MacOsBackend extends BaseSandboxBackend {
102
+ readonly os: "desktop-macos";
103
+ readonly tools: ReadonlySet<string>;
104
+ private readonly runner;
105
+ private readonly fs;
106
+ private readonly sleep;
107
+ private cliclickPath;
108
+ private cliclickProbed;
109
+ private forceFallback;
110
+ private cachedSize;
111
+ private retinaScale;
112
+ constructor(opts?: MacOsBackendOptions);
113
+ connect(): Promise<void>;
114
+ info(): Promise<DeviceInfo>;
115
+ screenshot(): Promise<ScreenshotOutput>;
116
+ screenSize(): Promise<ScreenSize>;
117
+ click(x: number, y: number, button?: "left" | "right" | "middle", clicks?: number): Promise<void>;
118
+ doubleClick(x: number, y: number): Promise<void>;
119
+ rightClick(x: number, y: number): Promise<void>;
120
+ tap(x: number, y: number): Promise<void>;
121
+ longPress(x: number, y: number, durationMs?: number): Promise<void>;
122
+ drag(x1: number, y1: number, x2: number, y2: number, _durationMs?: number): Promise<void>;
123
+ swipe(x1: number, y1: number, x2: number, y2: number, durationMs?: number): Promise<void>;
124
+ scroll(x: number, y: number, direction: "up" | "down" | "left" | "right", amount?: number): Promise<void>;
125
+ move(x: number, y: number): Promise<void>;
126
+ typeText(text: string): Promise<void>;
127
+ pressKey(key: string): Promise<void>;
128
+ hotkey(...keys: string[]): Promise<void>;
129
+ home(): Promise<void>;
130
+ launchApp(pkgOrName: string, _activity?: string): Promise<void>;
131
+ executeCommand(body: string, opts?: {
132
+ timeoutS?: number;
133
+ cwd?: string;
134
+ }): Promise<ExecuteCommandOutput>;
135
+ fileRead(path: string): Promise<Buffer>;
136
+ fileWrite(path: string, content: Buffer | string): Promise<void>;
137
+ listDirectory(path: string): Promise<ListDirectoryEntry[]>;
138
+ install(path: string): Promise<void>;
139
+ private useCliclick;
140
+ private probeCliclick;
141
+ private probeDisplay;
142
+ private runCliclick;
143
+ private runOsa;
144
+ /**
145
+ * Run a JXA (JavaScript for Automation) script via `osascript -l JavaScript`.
146
+ * Used for pointer / scroll synthesis — JXA's ObjC bridge gives us direct
147
+ * Quartz CGEvent access, which is the only dependency-free way to drive
148
+ * synthetic mouse input on modern macOS (the AppleScript `click at {x, y}`
149
+ * surface has been broken since the 10.14 era).
150
+ */
151
+ private runJxa;
152
+ private runUtf8;
153
+ private runBuffer;
154
+ }