dsh-mobilecode 0.1.4 → 0.2.1

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/README.md CHANGED
@@ -46,14 +46,47 @@ drawer:
46
46
  failed or incomplete), the Metro state, and `complete`.
47
47
  - `device_detect` — which platforms a directory supports, the framework, and
48
48
  the first attached Android device.
49
+ - A bundled **`device-ui-automation` playbook skill** (registered through
50
+ `ctx.skills.register()`, defensively — hosts without the skill service just
51
+ skip it): the observe-once → act-with-an-assertion → observe-again workflow,
52
+ which observer to reach for first, how to confirm an action landed via
53
+ `expect_text`/`expect_gone`, and the real-device safety rules.
49
54
  - `device_screen` — see what is on an attached Android device: a PNG screenshot
50
55
  plus the uiautomator UI hierarchy (text + pixel bounds) and **local PaddleOCR**
51
56
  text recognition (text + confidence + box), so an agent can read the screen
52
57
  and tap by coordinates. Also returns the foreground activity and screen size.
58
+ - `device_ui_tree` — the default screen observer: the uiautomator hierarchy as a
59
+ typed node tree (`type`/`text`/`contentDesc`/`resourceId`/`bounds`, with
60
+ `enabled`/`focused`/`clickable`/`scrollable` emitted only in their interesting
61
+ state), case-insensitive `filter` that keeps ancestors of matches, `max_depth`,
62
+ and a 40 KB cap that prunes the deepest levels first. Resource-ids are the most
63
+ stable tap handles.
64
+ - `device_tap_element` — tap a control by identity: `resource_id` matches the
65
+ node's resource-id, `text` matches its text or content-desc; exact match wins
66
+ over substring, nested duplicates collapse to the outermost control, ambiguity
67
+ lists up to 8 candidates instead of guessing, and disabled / off-screen nodes
68
+ are refused with the fix. `expect_text` / `expect_gone` verify the tap in the
69
+ same call — one round trip, no separate screenshot.
70
+ - `device_wait_for` — wait for text to appear or disappear: polls the UI tree
71
+ every ~600 ms and falls back to local PaddleOCR on textless surfaces (WebView /
72
+ Compose / canvas). A timeout is a normal `matched: false` result, never an
73
+ error — one call replaces an agent-side poll loop.
53
74
  - `device_input` — act on the device: tap / swipe / type / press a key at
54
75
  **absolute pixel coordinates** (the same space `device_screen` returns — take
55
76
  the box center `x=(x1+x2)/2, y=(y1+y2)/2`). The deterministic control loop is
56
- `device_screendevice_input device_screen`.
77
+ `device_ui_treedevice_tap_element`, falling back to
78
+ `device_screen → device_input` when a surface exposes no accessibility tree.
79
+ Typing is ASCII over plain adb; non-ASCII (CJK, emoji) is routed through the
80
+ ADBKeyboard IME when installed and refused with the install hint otherwise.
81
+ - `device_action` — device-level verbs beyond touches: `notifications`,
82
+ `quick_settings`, `collapse`, `lock`, `wake`, `assistant`, `rotate` (cycles
83
+ 0→90→180→270 and pins auto-rotate off).
84
+ - `device_boot` / `device_shutdown` — boot an AVD by name and wait until it
85
+ finishes booting (adopts a running emulator for the same AVD) / shut an
86
+ emulator down (`adb emu kill`; refuses physical devices).
87
+ - `device_apps` / `device_launch_app` — list installed packages (third-party by
88
+ default) so a package name is never guessed / launch one by package or a
89
+ unique substring, with `relaunch` for a cold start.
57
90
  - `device_log` — device logs: logcat `main`/`crash`/`events`/`kernel` buffers
58
91
  (kernel = dmesg, needs adb root — works on emulators) with an optional
59
92
  case-insensitive substring filter, capped line count.
@@ -108,11 +108,19 @@ export async function capture(command, args, options = {}) {
108
108
  })
109
109
  running.stdout?.setEncoding("utf8")
110
110
  running.stdout?.on("data", (chunk) => out.push(chunk))
111
+ let timer
112
+ if (options.timeoutMs) {
113
+ timer = setTimeout(() => running.kill(), options.timeoutMs)
114
+ timer.unref?.()
115
+ }
111
116
  const code = await new Promise((resolve) => {
112
117
  running.once("error", () => resolve(-1))
113
118
  running.once("close", (value) => resolve(value ?? -1))
114
119
  })
115
- return code === 0 ? out.join("") : ""
120
+ if (timer) clearTimeout(timer)
121
+ if (code !== 0) return ""
122
+ const text = out.join("")
123
+ return options.maxBytes && Buffer.byteLength(text) > options.maxBytes ? text.slice(0, options.maxBytes) : text
116
124
  }
117
125
 
118
126
  /** Direct child pids of `pid`. POSIX only; returns nothing when pgrep is unavailable. */
@@ -732,6 +740,57 @@ export async function androidBooted(serial) {
732
740
  return output.trim() === "1"
733
741
  }
734
742
 
743
+ /** AVD name of an emulator serial (`adb emu avd name`); undefined for physical/offline. */
744
+ export async function avdName(serial) {
745
+ const output = await capture(adb(), ["-s", serial, "emu", "avd", "name"])
746
+ const line = output
747
+ .split(/\r?\n/)
748
+ .map((item) => item.trim())
749
+ .find((item) => item && !/^OK$/i.test(item) && !/^KO:/i.test(item))
750
+ return line && /^[A-Za-z0-9._-]+$/.test(line) ? line : undefined
751
+ }
752
+
753
+ /** Poll until sys.boot_completed == 1 or the timeout; true when booted. */
754
+ export async function waitForBoot(serial, timeoutMs = 180_000) {
755
+ const deadline = Date.now() + timeoutMs
756
+ for (;;) {
757
+ if (await androidBooted(serial)) return true
758
+ if (Date.now() >= deadline) return false
759
+ await new Promise((resolve) => setTimeout(resolve, 1000))
760
+ }
761
+ }
762
+
763
+ /** Launch an AVD detached (survives this process); undefined when no emulator binary. */
764
+ export function bootEmulator(avd) {
765
+ const emulator = emulatorBinary()
766
+ if (!emulator || !/^[A-Za-z0-9._-]+$/.test(avd)) return undefined
767
+ const child = launch(emulator, ["-avd", avd], { stdio: "ignore", detached: true })
768
+ child.unref?.()
769
+ return child
770
+ }
771
+
772
+ /** True when the ADBKeyboard IME is installed (the only way to type non-ASCII over adb). */
773
+ export async function adbKeyboardReady(serial) {
774
+ const output = await capture(adb(), ["-s", serial, "shell", "ime", "list", "-s"])
775
+ return /com\.android\.adbkeyboard/i.test(output)
776
+ }
777
+
778
+ /** Type one string via the ADBKeyboard broadcast (base64, so any codepoint survives the shell). */
779
+ export async function typeViaAdbKeyboard(serial, text) {
780
+ const msg = Buffer.from(text, "utf8").toString("base64")
781
+ await exec(adb(), ["-s", serial, "shell", "am", "broadcast", "-a", "ADB_INPUT_B64", "--es", "msg", msg]).exit
782
+ }
783
+
784
+ /** True when every codepoint is safe for `adb shell input text` (printable ASCII, no shell metachars). */
785
+ export function isAsciiInput(text) {
786
+ return [...text].every((ch) => ch.codePointAt(0) >= 0x20 && ch.codePointAt(0) <= 0x7e)
787
+ }
788
+
789
+ /** Escape one `input text` argument for the device shell: backslash the metachars, spaces to %s. */
790
+ export function escapeInputText(text) {
791
+ return text.replace(/[\\()<>|;&*~"'`$#!{}[\]]/g, "\\$&").replace(/ /g, "%s")
792
+ }
793
+
735
794
  function aapt2() {
736
795
  const sdk = androidSdk()
737
796
  if (!sdk) return undefined