@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.
- package/LICENSE +201 -0
- package/dist/backends/android-adb-runner.d.ts +32 -0
- package/dist/backends/android-adb-runner.js +15 -0
- package/dist/backends/android-adb-runner.js.map +1 -0
- package/dist/backends/android-adb.d.ts +153 -0
- package/dist/backends/android-adb.js +723 -0
- package/dist/backends/android-adb.js.map +1 -0
- package/dist/backends/base.d.ts +150 -0
- package/dist/backends/base.js +116 -0
- package/dist/backends/base.js.map +1 -0
- package/dist/backends/desktop.d.ts +62 -0
- package/dist/backends/desktop.js +176 -0
- package/dist/backends/desktop.js.map +1 -0
- package/dist/backends/index.d.ts +63 -0
- package/dist/backends/index.js +105 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/linux.d.ts +69 -0
- package/dist/backends/linux.js +230 -0
- package/dist/backends/linux.js.map +1 -0
- package/dist/backends/mac.d.ts +154 -0
- package/dist/backends/mac.js +881 -0
- package/dist/backends/mac.js.map +1 -0
- package/dist/backends/stubs/ios.d.ts +17 -0
- package/dist/backends/stubs/ios.js +32 -0
- package/dist/backends/stubs/ios.js.map +1 -0
- package/dist/backends/stubs/macos.d.ts +13 -0
- package/dist/backends/stubs/macos.js +27 -0
- package/dist/backends/stubs/macos.js.map +1 -0
- package/dist/backends/stubs/windows.d.ts +69 -0
- package/dist/backends/stubs/windows.js +191 -0
- package/dist/backends/stubs/windows.js.map +1 -0
- package/dist/cli.d.ts +37 -0
- package/dist/cli.js +177 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/server/action-mapping.d.ts +21 -0
- package/dist/server/action-mapping.js +153 -0
- package/dist/server/action-mapping.js.map +1 -0
- package/dist/server/app.d.ts +23 -0
- package/dist/server/app.js +157 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/tool-registry.d.ts +50 -0
- package/dist/server/tool-registry.js +504 -0
- package/dist/server/tool-registry.js.map +1 -0
- package/dist/util/adb-files.d.ts +92 -0
- package/dist/util/adb-files.js +221 -0
- package/dist/util/adb-files.js.map +1 -0
- package/dist/util/adb-shell.d.ts +80 -0
- package/dist/util/adb-shell.js +102 -0
- package/dist/util/adb-shell.js.map +1 -0
- package/dist/util/android-apps.d.ts +10 -0
- package/dist/util/android-apps.js +103 -0
- package/dist/util/android-apps.js.map +1 -0
- package/dist/util/image-dim.d.ts +27 -0
- package/dist/util/image-dim.js +37 -0
- package/dist/util/image-dim.js.map +1 -0
- package/dist/util/ui-xml.d.ts +20 -0
- package/dist/util/ui-xml.js +184 -0
- package/dist/util/ui-xml.js.map +1 -0
- 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":"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"}
|
|
@@ -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
|
+
}
|