@beeos-ai/device-mcp-server 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/backends/android-adb.d.ts +147 -6
  2. package/dist/backends/android-adb.js +776 -40
  3. package/dist/backends/android-adb.js.map +1 -1
  4. package/dist/backends/base.d.ts +243 -7
  5. package/dist/backends/base.js +81 -2
  6. package/dist/backends/base.js.map +1 -1
  7. package/dist/backends/desktop.d.ts +3 -2
  8. package/dist/backends/desktop.js +9 -3
  9. package/dist/backends/desktop.js.map +1 -1
  10. package/dist/backends/linux.js +3 -0
  11. package/dist/backends/linux.js.map +1 -1
  12. package/dist/backends/mac.d.ts +11 -2
  13. package/dist/backends/mac.js +39 -1
  14. package/dist/backends/mac.js.map +1 -1
  15. package/dist/backends/stubs/windows.js +3 -0
  16. package/dist/backends/stubs/windows.js.map +1 -1
  17. package/dist/cli.d.ts +40 -26
  18. package/dist/cli.js +118 -84
  19. package/dist/cli.js.map +1 -1
  20. package/dist/index.d.ts +9 -6
  21. package/dist/index.js +9 -6
  22. package/dist/index.js.map +1 -1
  23. package/dist/server/app.d.ts +60 -17
  24. package/dist/server/app.js +182 -138
  25. package/dist/server/app.js.map +1 -1
  26. package/dist/server/mcp-server.d.ts +25 -0
  27. package/dist/server/mcp-server.js +33 -0
  28. package/dist/server/mcp-server.js.map +1 -0
  29. package/dist/server/registry.d.ts +111 -0
  30. package/dist/server/registry.js +191 -0
  31. package/dist/server/registry.js.map +1 -0
  32. package/dist/server/stdio.d.ts +29 -0
  33. package/dist/server/stdio.js +35 -0
  34. package/dist/server/stdio.js.map +1 -0
  35. package/dist/server/tool-registry.d.ts +60 -35
  36. package/dist/server/tool-registry.js +911 -434
  37. package/dist/server/tool-registry.js.map +1 -1
  38. package/dist/util/adb-files.d.ts +25 -1
  39. package/dist/util/adb-files.js +95 -0
  40. package/dist/util/adb-files.js.map +1 -1
  41. package/dist/util/locale.d.ts +16 -0
  42. package/dist/util/locale.js +31 -0
  43. package/dist/util/locale.js.map +1 -0
  44. package/dist/util/logger.d.ts +27 -0
  45. package/dist/util/logger.js +27 -0
  46. package/dist/util/logger.js.map +1 -0
  47. package/dist/util/output-path.d.ts +60 -0
  48. package/dist/util/output-path.js +123 -0
  49. package/dist/util/output-path.js.map +1 -0
  50. package/dist/util/package-name.d.ts +26 -0
  51. package/dist/util/package-name.js +41 -0
  52. package/dist/util/package-name.js.map +1 -0
  53. package/package.json +5 -3
  54. package/dist/server/action-mapping.d.ts +0 -21
  55. package/dist/server/action-mapping.js +0 -153
  56. package/dist/server/action-mapping.js.map +0 -1
@@ -49,10 +49,31 @@
49
49
  * system). The Android backend no longer compresses by default — the
50
50
  * agent layer decides whether to resize.
51
51
  */
52
- import { DeviceError, type DeviceInfo } from "@beeos-ai/device-common";
53
- import { BaseSandboxBackend, type ExecuteCommandOutput, type ListDirectoryEntry, type ScreenSize, type ScreenshotOutput, type UiDumpOutput } from "./base.js";
52
+ import { type ChildProcess, type SpawnOptions } from "node:child_process";
53
+ import { DeviceError, type AppInfo, type CrashSummary, type DeviceInfo, type Orientation, type SaveScreenshotResult, type ScreenRecordHandle, type ScreenRecordResult, type UiElement } from "@beeos-ai/device-common";
54
+ import { BaseSandboxBackend, type ExecuteCommandOutput, type LaunchAppOptions, type StartScreenRecordOptions, type ListDirectoryEntry, type ScreenSize, type ScreenshotOptions, type ScreenshotOutput, type UiDumpOutput } from "./base.js";
54
55
  import { type AdbRunOptions, type AdbRunResult, type AdbRunner } from "./android-adb-runner.js";
55
56
  export type { AdbRunOptions, AdbRunResult, AdbRunner };
57
+ /**
58
+ * Friendly button name → Android `KEYCODE_*`. Names are case-insensitive.
59
+ *
60
+ * The vocabulary mirrors mobile-mcp `mobile_press_button` so prompts that
61
+ * already work against mobile-mcp port over without rewording. The
62
+ * registry exposes `Object.keys(PRESS_BUTTON_KEYMAP)` so the MCP tool's
63
+ * `enum` is auto-derived rather than maintained in two places.
64
+ */
65
+ export declare const PRESS_BUTTON_KEYMAP: Readonly<Record<string, string>>;
66
+ /**
67
+ * Pluggable spawner used by `screen_record_*` to fork a long-lived
68
+ * `adb shell screenrecord` child. Mirrors `node:child_process.spawn`'s
69
+ * shape — exposed so tests can fake the recording lifecycle without
70
+ * actually invoking adb.
71
+ *
72
+ * Mutable `string[]` (not `readonly`) on purpose — `node:child_process.spawn`
73
+ * itself takes a mutable array, and using `readonly` here would force
74
+ * every implementation through a type assertion.
75
+ */
76
+ export type AdbProcessSpawner = (file: string, args: string[], opts?: SpawnOptions) => ChildProcess;
56
77
  export interface AndroidAdbBackendOptions {
57
78
  /** ADB serial, e.g. `emulator-5554`. Optional when a single device is connected. */
58
79
  serial?: string;
@@ -72,6 +93,22 @@ export interface AndroidAdbBackendOptions {
72
93
  fileTimeoutMs?: number;
73
94
  /** Sleep helper, injectable for deterministic tests. */
74
95
  sleep?: (ms: number) => Promise<void>;
96
+ /**
97
+ * Long-process spawner for `screen_record_*`. Defaults to
98
+ * `node:child_process.spawn`.
99
+ */
100
+ processSpawner?: AdbProcessSpawner;
101
+ /**
102
+ * Hard host-side timeout for waiting on a recording to flush after
103
+ * SIGINT before escalating to SIGKILL. Default 5_000 ms.
104
+ */
105
+ recordKillGraceMs?: number;
106
+ /**
107
+ * Device-side hard cap forwarded as `screenrecord --time-limit`.
108
+ * Default 300 (5 minutes — also adb's own legacy cap so any value
109
+ * above that is silently ignored).
110
+ */
111
+ recordTimeLimitS?: number;
75
112
  }
76
113
  export declare class AndroidAdbBackend extends BaseSandboxBackend {
77
114
  readonly os: "android";
@@ -85,11 +122,29 @@ export declare class AndroidAdbBackend extends BaseSandboxBackend {
85
122
  private readonly screenshotTimeoutMs;
86
123
  private readonly installTimeoutMs;
87
124
  private readonly fileTimeoutMs;
125
+ private readonly processSpawner;
126
+ private readonly recordKillGraceMs;
127
+ private readonly recordTimeLimitS;
128
+ private readonly recordings;
129
+ /** Cached `android.software.leanback` system feature — drives `info().isTv`. */
130
+ private isLeanback;
131
+ /** True after `probeLeanback()` has resolved (success OR failure). */
132
+ private leanbackProbed;
88
133
  constructor(opts?: AndroidAdbBackendOptions);
89
134
  connect(): Promise<void>;
135
+ /**
136
+ * Detect Android TV / leanback so `info().isTv` can hint at LLM
137
+ * prompts that should default to DPAD navigation. Cached for the
138
+ * lifetime of the backend — running `pm has-feature` once per device
139
+ * is enough; the answer doesn't change at runtime. Lazy on purpose —
140
+ * `connect()` with `skipConnectValidation: true` (the registry path)
141
+ * MUST NOT issue any adb commands, so we defer this probe to the
142
+ * first `info()` call.
143
+ */
144
+ private ensureLeanbackProbed;
90
145
  disconnect(): Promise<void>;
91
146
  info(): Promise<DeviceInfo>;
92
- screenshot(): Promise<ScreenshotOutput>;
147
+ screenshot(opts?: ScreenshotOptions): Promise<ScreenshotOutput>;
93
148
  screenSize(): Promise<ScreenSize>;
94
149
  uiDump(): Promise<UiDumpOutput>;
95
150
  tap(x: number, y: number): Promise<void>;
@@ -98,13 +153,24 @@ export declare class AndroidAdbBackend extends BaseSandboxBackend {
98
153
  longPress(x: number, y: number, durationMs?: number): Promise<void>;
99
154
  swipe(x1: number, y1: number, x2: number, y2: number, durationMs?: number): Promise<void>;
100
155
  drag(x1: number, y1: number, x2: number, y2: number, durationMs?: number): Promise<void>;
101
- scroll(x: number, y: number, direction: "up" | "down" | "left" | "right", _amount?: number): Promise<void>;
156
+ scroll(x: number, y: number, direction: "up" | "down" | "left" | "right", amount?: number): Promise<void>;
102
157
  typeText(text: string): Promise<void>;
103
158
  pressKey(key: string): Promise<void>;
104
159
  back(): Promise<void>;
105
160
  home(): Promise<void>;
106
161
  navigate(direction: "back" | "forward" | "up"): Promise<void>;
107
- launchApp(pkgOrName: string, activity?: string): Promise<void>;
162
+ launchApp(pkgOrName: string, opts?: LaunchAppOptions | string): Promise<void>;
163
+ openUrl(url: string): Promise<void>;
164
+ listApps(opts?: {
165
+ includeSystem?: boolean;
166
+ launchableOnly?: boolean;
167
+ }): Promise<AppInfo[]>;
168
+ terminateApp(pkg: string): Promise<void>;
169
+ listElements(opts?: {
170
+ query?: string;
171
+ }): Promise<UiElement[]>;
172
+ getOrientation(): Promise<Orientation>;
173
+ setOrientation(o: Orientation): Promise<void>;
108
174
  executeCommand(body: string, opts?: {
109
175
  timeoutS?: number;
110
176
  cwd?: string;
@@ -113,7 +179,55 @@ export declare class AndroidAdbBackend extends BaseSandboxBackend {
113
179
  fileWrite(path: string, content: Buffer | string): Promise<void>;
114
180
  listDirectory(path: string): Promise<ListDirectoryEntry[]>;
115
181
  install(path: string): Promise<void>;
116
- private getCurrentApp;
182
+ uninstallApp(pkgOrName: string): Promise<void>;
183
+ /**
184
+ * Parse `dumpsys dropbox --print` and return one entry per recorded
185
+ * crash / ANR / WTF. The list is best-effort — the dumpsys output
186
+ * format has shifted slightly across Android releases, so we tolerate
187
+ * unknown lines instead of rejecting them.
188
+ */
189
+ listCrashes(): Promise<CrashSummary[]>;
190
+ /**
191
+ * Re-run `dumpsys dropbox --print` and return the body of the entry
192
+ * whose `id` matches. Stateless so the result stays correct even if
193
+ * `listCrashes` was called from a different MCP session.
194
+ */
195
+ getCrash(id: string): Promise<string>;
196
+ /**
197
+ * Spawn `adb shell screenrecord` for a single recording. The device
198
+ * enforces a hard `--time-limit` (default 5 min, capped at 300s by
199
+ * Android itself); the host sends SIGINT on `stopScreenRecord` and
200
+ * falls back to SIGKILL after `recordKillGraceMs` if the child
201
+ * hasn't exited.
202
+ *
203
+ * The same-pid `screenrecord` invocation also lets us pipe the output
204
+ * to a `/sdcard/<id>.mp4` so `adb pull` after stop can fetch the
205
+ * exact file without guessing names.
206
+ *
207
+ * `opts.timeLimitS` (mobile-mcp parity, r3): caller-supplied cap,
208
+ * clamped into `[1, 300]` and passed to `screenrecord --time-limit`.
209
+ * `opts.localPath`: caller-supplied host-side destination registered
210
+ * at start time. Already passed through `assertSafeOutputPathWithExt`
211
+ * — backend treats as trusted.
212
+ */
213
+ startScreenRecord(opts?: StartScreenRecordOptions): Promise<ScreenRecordHandle>;
214
+ stopScreenRecord(id: string, opts?: {
215
+ path?: string;
216
+ }): Promise<ScreenRecordResult>;
217
+ /**
218
+ * Capture a screenshot via `adb pull` of `/sdcard/screenshot.png`.
219
+ * Bypasses the JSON-RPC body cap by writing the bytes straight to a
220
+ * host filesystem path the caller chose. Useful for 4K Android
221
+ * captures where base64-over-HTTP would otherwise blow past 16 MB.
222
+ */
223
+ saveScreenshot(localPath: string, opts?: ScreenshotOptions): Promise<SaveScreenshotResult>;
224
+ /**
225
+ * Count the number of displays the device exposes. Reads
226
+ * `dumpsys SurfaceFlinger --display-id` (Android 10+) and falls back
227
+ * to "1" on older builds where the flag isn't recognised.
228
+ */
229
+ getDisplayCount(): Promise<number>;
230
+ getCurrentApp(): Promise<string>;
117
231
  private getCurrentIme;
118
232
  private setIme;
119
233
  private clearText;
@@ -151,3 +265,30 @@ export declare function listAdbDevices(runner?: AdbRunner, timeoutMs?: number):
151
265
  * a `DeviceError` in another `DeviceError`.
152
266
  */
153
267
  export declare function wrapAdbFailure(verb: string, args: string[], cause: unknown, timeoutMs?: number): DeviceError;
268
+ export declare function assertSafeUrl(url: string): void;
269
+ /**
270
+ * Parse the textual output of `dumpsys dropbox --print` into one
271
+ * `CrashSummary` per crash / ANR / WTF entry.
272
+ *
273
+ * Output shape (Android 7+):
274
+ *
275
+ * 2024-08-15 12:34:56 system_app_crash (text, 1234 bytes):
276
+ * Process: com.foo.bar
277
+ * ...stack trace lines...
278
+ * 2024-08-15 12:35:00 data_app_anr (text, 5678 bytes):
279
+ * ...
280
+ *
281
+ * We extract the leading `<timestamp> <tag> (text, N bytes):` row and
282
+ * include the first non-blank body line as a `headline` for at-a-glance
283
+ * triage. The id encodes both the tag and the timestamp so `getCrash`
284
+ * can re-locate the same row on the next `dumpsys` invocation without
285
+ * holding state.
286
+ */
287
+ export declare function parseDropboxEntries(out: string): CrashSummary[];
288
+ /**
289
+ * Pull the body of a single dumpsys-dropbox entry out of the `--print`
290
+ * blob, indexed by `<tag>:<timestamp>` (the same id `parseDropboxEntries`
291
+ * returns). The body is everything between the matching header and the
292
+ * next entry header.
293
+ */
294
+ export declare function extractDropboxBody(out: string, id: string): string | undefined;