dsh-mobilecode 0.2.0 → 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
@@ -67,11 +67,26 @@ drawer:
67
67
  lists up to 8 candidates instead of guessing, and disabled / off-screen nodes
68
68
  are refused with the fix. `expect_text` / `expect_gone` verify the tap in the
69
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.
70
74
  - `device_input` — act on the device: tap / swipe / type / press a key at
71
75
  **absolute pixel coordinates** (the same space `device_screen` returns — take
72
76
  the box center `x=(x1+x2)/2, y=(y1+y2)/2`). The deterministic control loop is
73
77
  `device_ui_tree → device_tap_element`, falling back to
74
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.
75
90
  - `device_log` — device logs: logcat `main`/`crash`/`events`/`kernel` buffers
76
91
  (kernel = dmesg, needs adb root — works on emulators) with an optional
77
92
  case-insensitive substring filter, capped line count.
@@ -740,6 +740,57 @@ export async function androidBooted(serial) {
740
740
  return output.trim() === "1"
741
741
  }
742
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
+
743
794
  function aapt2() {
744
795
  const sdk = androidSdk()
745
796
  if (!sdk) return undefined
package/lib/index.js CHANGED
@@ -583,9 +583,21 @@ function deviceInputTool() {
583
583
  }
584
584
  case 'text': {
585
585
  if (typeof args.text !== 'string' || args.text.length === 0) throw new Error('action=text requires a non-empty text string.')
586
- const escaped = args.text.replace(/\s/g, '%s')
587
- await DeviceBuild.exec(DeviceBuild.adb(), adbArgs(['input', 'text', escaped])).exit
588
- return { serial, action, sent: `text "${args.text}"` }
586
+ if (DeviceBuild.isAsciiInput(args.text)) {
587
+ await DeviceBuild.exec(DeviceBuild.adb(), adbArgs(['input', 'text', DeviceBuild.escapeInputText(args.text)])).exit
588
+ return { serial, action, sent: `text "${args.text}"` }
589
+ }
590
+ // Non-ASCII (CJK, emoji, accented) cannot go through `input text`; the
591
+ // ADBKeyboard IME is the only adb path. Refuse with the fix, never mangle.
592
+ if (!(await DeviceBuild.adbKeyboardReady(serial))) {
593
+ throw new Error(
594
+ `device_input cannot type non-ASCII text ("${args.text}") over plain adb. Install ADBKeyboard `
595
+ + 'on the device (https://github.com/senzhk/ADBKeyBoard), enable it (ime enable/set '
596
+ + 'com.android.adbkeyboard/.AdbIME), then retry — this tool then types via its base64 broadcast.',
597
+ )
598
+ }
599
+ await DeviceBuild.typeViaAdbKeyboard(serial, args.text)
600
+ return { serial, action, sent: `text "${args.text}" (ADBKeyboard)` }
589
601
  }
590
602
  case 'key': {
591
603
  const raw = String(args.key ?? '')
@@ -601,6 +613,301 @@ function deviceInputTool() {
601
613
  })
602
614
  }
603
615
 
616
+ /** OCR the current screen and report whether `wantedLower` appears; undefined when OCR is unavailable. */
617
+ async function ocrHasText(serial, wantedLower) {
618
+ if (!DeviceBuild.ocrPython()) return undefined
619
+ const png = await DeviceBuild.screenCapture(serial)
620
+ if (!png) return undefined
621
+ const ocr = await DeviceBuild.ocrImage(png).catch(() => [])
622
+ return ocr.some((item) => String(item.text).toLowerCase().includes(wantedLower))
623
+ }
624
+
625
+ function deviceWaitForTool() {
626
+ return defineTool({
627
+ name: 'device_wait_for',
628
+ description: 'Wait for on-screen text to appear or disappear. Polls the uiautomator tree every ~600 ms; when the tree ' +
629
+ 'carries no labels (WebView/Compose/canvas) it falls back to local PaddleOCR. A timeout is a normal matched:false ' +
630
+ 'result, never an error — one call replaces an agent-side poll loop.',
631
+ parameters: {
632
+ serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
633
+ text: { type: 'string', description: 'Text to wait for (case-insensitive substring).' },
634
+ mode: { type: 'string', enum: ['appear', 'disappear'], description: 'appear (default) waits for the text; disappear waits for it to go away.' },
635
+ timeout_ms: { type: 'integer', description: 'Max wait in ms (default 10000, max 60000).' },
636
+ },
637
+ output: {
638
+ schema: {
639
+ type: 'object',
640
+ additionalProperties: false,
641
+ properties: {
642
+ serial: { type: 'string', required: true },
643
+ text: { type: 'string', required: true },
644
+ mode: { type: 'string', required: true },
645
+ matched: { type: 'boolean', required: true },
646
+ waited_ms: { type: 'integer', required: true },
647
+ source: { type: 'string', required: true },
648
+ },
649
+ },
650
+ render: (_args, value) => {
651
+ const v = value ?? { serial: '', text: '', mode: 'appear', matched: false, waited_ms: 0, source: 'ui_tree' }
652
+ return [{ type: 'text', text: `${v.matched ? 'MATCHED' : 'TIMEOUT'}: "${v.text}" ${v.mode} (${v.source}) after ${v.waited_ms}ms on ${v.serial}` }]
653
+ },
654
+ },
655
+ async execute(args) {
656
+ const serial = await requireAndroidDevice(args.serial)
657
+ const text = String(args.text ?? '').trim()
658
+ if (text === '') throw new Error('device_wait_for requires a non-empty text.')
659
+ const mode = args.mode === 'disappear' ? 'disappear' : 'appear'
660
+ const timeout = Math.min(Math.max(args.timeout_ms ?? 10_000, 500), 60_000)
661
+ const wanted = text.toLowerCase()
662
+ const start = Date.now()
663
+ const deadline = start + timeout
664
+ let source = 'ui_tree'
665
+ for (;;) {
666
+ let labels
667
+ try {
668
+ labels = UiTree.collectLabels((await UiTree.readUiTree(serial)).roots)
669
+ } catch {
670
+ labels = undefined
671
+ }
672
+ let present
673
+ if (labels !== undefined && labels.length > 0) {
674
+ source = 'ui_tree'
675
+ present = labels.some((label) => label.toLowerCase().includes(wanted))
676
+ } else {
677
+ const ocr = await ocrHasText(serial, wanted)
678
+ if (ocr === undefined) { source = 'ui_tree'; present = false }
679
+ else { source = 'ocr'; present = ocr }
680
+ }
681
+ const matched = mode === 'appear' ? present : !present
682
+ const waited = Math.min(timeout, Date.now() - start)
683
+ if (matched) return { serial, text, mode, matched: true, waited_ms: waited, source }
684
+ if (Date.now() >= deadline) return { serial, text, mode, matched: false, waited_ms: timeout, source }
685
+ await new Promise((resolve) => setTimeout(resolve, source === 'ocr' ? 2000 : 600))
686
+ }
687
+ },
688
+ })
689
+ }
690
+
691
+ function deviceBootTool() {
692
+ return defineTool({
693
+ name: 'device_boot',
694
+ description: 'Boot an Android emulator AVD by name (from device_status.avds) and wait until it finishes booting. ' +
695
+ 'If an emulator for that AVD is already running it is adopted. This is the device-centric boot — device_run is the ' +
696
+ 'project-centric build+install+launch.',
697
+ parameters: {
698
+ avd: { type: 'string', description: 'AVD name to boot.' },
699
+ timeout_ms: { type: 'integer', description: 'Max wait for boot in ms (default 180000, max 600000).' },
700
+ },
701
+ output: {
702
+ schema: {
703
+ type: 'object',
704
+ additionalProperties: false,
705
+ properties: {
706
+ serial: { type: 'string', required: true },
707
+ avd: { type: 'string', required: true },
708
+ booted: { type: 'boolean', required: true },
709
+ alreadyRunning: { type: 'boolean', required: true },
710
+ },
711
+ },
712
+ render: (_args, value) => {
713
+ const v = value ?? { serial: '', avd: '', booted: false, alreadyRunning: false }
714
+ return [{ type: 'text', text: `${v.alreadyRunning ? 'Adopted running' : 'Booted'} emulator ${v.serial} (AVD ${v.avd}), boot completed: ${v.booted}` }]
715
+ },
716
+ },
717
+ async execute(args) {
718
+ const avd = String(args.avd ?? '').trim()
719
+ if (avd === '') throw new Error('device_boot requires an avd name (see device_status.avds).')
720
+ const timeout = Math.min(Math.max(args.timeout_ms ?? 180_000, 5_000), 600_000)
721
+ const rows = await DeviceBuild.devices()
722
+ for (const row of rows.filter((item) => item.state === 'device' && item.serial.startsWith('emulator-'))) {
723
+ const name = await DeviceBuild.avdName(row.serial).catch(() => undefined)
724
+ if (name === avd) {
725
+ const booted = await DeviceBuild.waitForBoot(row.serial, timeout)
726
+ return { serial: row.serial, avd, booted, alreadyRunning: true }
727
+ }
728
+ }
729
+ if (!DeviceBuild.emulatorBinary()) throw new Error('No SDK emulator binary found; cannot boot an AVD.')
730
+ const before = new Set(rows.map((item) => item.serial))
731
+ if (!DeviceBuild.bootEmulator(avd)) throw new Error(`Could not launch the emulator for AVD "${avd}".`)
732
+ const deadline = Date.now() + timeout
733
+ let serial
734
+ while (Date.now() < deadline) {
735
+ const now = await DeviceBuild.devices()
736
+ const fresh = now.find((item) => item.serial.startsWith('emulator-') && !before.has(item.serial))
737
+ if (fresh) { serial = fresh.serial; break }
738
+ await new Promise((resolve) => setTimeout(resolve, 1000))
739
+ }
740
+ if (!serial) throw new Error(`No emulator serial appeared for "${avd}" within ${timeout} ms.`)
741
+ const booted = await DeviceBuild.waitForBoot(serial, Math.max(1000, deadline - Date.now()))
742
+ if (!booted) throw new Error(`Emulator ${serial} ("${avd}") did not finish booting within ${timeout} ms.`)
743
+ return { serial, avd, booted: true, alreadyRunning: false }
744
+ },
745
+ })
746
+ }
747
+
748
+ function deviceShutdownTool() {
749
+ return defineTool({
750
+ name: 'device_shutdown',
751
+ description: 'Shut down an emulator (`adb emu kill`). Refuses physical devices — adb has no power-off verb for phones.',
752
+ parameters: {
753
+ serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
754
+ },
755
+ output: {
756
+ schema: {
757
+ type: 'object',
758
+ additionalProperties: false,
759
+ properties: {
760
+ serial: { type: 'string', required: true },
761
+ shutdown: { type: 'boolean', required: true },
762
+ },
763
+ },
764
+ render: (_args, value) => [{ type: 'text', text: `Shut down emulator ${value?.serial}` }],
765
+ },
766
+ async execute(args) {
767
+ const serial = await requireAndroidDevice(args.serial)
768
+ const isEmulator = serial.startsWith('emulator-') || (await DeviceBuild.avdName(serial).catch(() => undefined)) !== undefined
769
+ if (!isEmulator) {
770
+ throw new Error(`device_shutdown refuses ${serial}: it is a physical device and adb has no power-off verb for phones — use its own power button.`)
771
+ }
772
+ await DeviceBuild.exec(DeviceBuild.adb(), ['-s', serial, 'emu', 'kill']).exit
773
+ return { serial, shutdown: true }
774
+ },
775
+ })
776
+ }
777
+
778
+ const DEVICE_ACTIONS = {
779
+ notifications: ['cmd', 'statusbar', 'expand-notifications'],
780
+ quick_settings: ['cmd', 'statusbar', 'expand-settings'],
781
+ collapse: ['cmd', 'statusbar', 'collapse'],
782
+ lock: ['input', 'keyevent', '223'],
783
+ wake: ['input', 'keyevent', '224'],
784
+ assistant: ['am', 'start', '-a', 'android.intent.action.ASSIST'],
785
+ }
786
+
787
+ function deviceActionTool() {
788
+ return defineTool({
789
+ name: 'device_action',
790
+ description: 'Device-level actions beyond touches: open the notification shade or quick settings, collapse the shade, ' +
791
+ 'lock or wake the screen, launch the assistant, or rotate the display (cycles 0→90→180→270 and pins auto-rotate off).',
792
+ parameters: {
793
+ serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
794
+ action: { type: 'string', enum: [...Object.keys(DEVICE_ACTIONS), 'rotate'], description: 'Which device action to perform.' },
795
+ },
796
+ output: {
797
+ schema: {
798
+ type: 'object',
799
+ additionalProperties: false,
800
+ properties: {
801
+ serial: { type: 'string', required: true },
802
+ action: { type: 'string', required: true },
803
+ rotation: { type: 'integer' },
804
+ },
805
+ },
806
+ render: (_args, value) => [{ type: 'text', text: value?.rotation !== undefined ? `${value.action} on ${value.serial} → rotation ${value.rotation * 90}°` : `${value?.action} on ${value?.serial}` }],
807
+ },
808
+ async execute(args) {
809
+ const serial = await requireAndroidDevice(args.serial)
810
+ const action = String(args.action ?? '')
811
+ if (action === 'rotate') {
812
+ const current = Number(await DeviceBuild.capture(DeviceBuild.adb(), ['-s', serial, 'shell', 'settings', 'get', 'system', 'user_rotation']))
813
+ const next = ((Number.isFinite(current) ? current : 0) + 1) % 4
814
+ await DeviceBuild.exec(DeviceBuild.adb(), ['-s', serial, 'shell', 'settings', 'put', 'system', 'accelerometer_rotation', '0']).exit
815
+ await DeviceBuild.exec(DeviceBuild.adb(), ['-s', serial, 'shell', 'settings', 'put', 'system', 'user_rotation', String(next)]).exit
816
+ return { serial, action, rotation: next }
817
+ }
818
+ const shell = DEVICE_ACTIONS[action]
819
+ if (!shell) throw new Error(`unknown action "${action}" — use ${[...Object.keys(DEVICE_ACTIONS), 'rotate'].join(', ')}.`)
820
+ await DeviceBuild.exec(DeviceBuild.adb(), ['-s', serial, 'shell', ...shell]).exit
821
+ return { serial, action }
822
+ },
823
+ })
824
+ }
825
+
826
+ function deviceAppsTool() {
827
+ return defineTool({
828
+ name: 'device_apps',
829
+ description: 'List installed Android packages (third-party by default; include_system=true adds platform apps). ' +
830
+ 'Use it to find the real package name before device_launch_app — never guess one.',
831
+ parameters: {
832
+ serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
833
+ include_system: { type: 'boolean', description: 'Include system/platform packages (default false).' },
834
+ },
835
+ output: {
836
+ schema: {
837
+ type: 'object',
838
+ additionalProperties: false,
839
+ properties: {
840
+ serial: { type: 'string', required: true },
841
+ count: { type: 'integer', required: true },
842
+ packages: { type: 'array', required: true, items: { type: 'string' } },
843
+ },
844
+ },
845
+ render: (_args, value) => {
846
+ const v = value ?? { serial: '', count: 0, packages: [] }
847
+ const shown = v.packages.slice(0, 60).join('\n ')
848
+ return [{ type: 'text', text: `${v.serial}: ${v.count} packages\n ${shown}${v.count > 60 ? `\n … and ${v.count - 60} more` : ''}` }]
849
+ },
850
+ },
851
+ async execute(args) {
852
+ const serial = await requireAndroidDevice(args.serial)
853
+ const output = await DeviceBuild.capture(DeviceBuild.adb(), ['-s', serial, 'shell', 'pm', 'list', 'packages', ...(args.include_system ? [] : ['-3'])])
854
+ const packages = output
855
+ .split(/\r?\n/)
856
+ .map((line) => line.trim())
857
+ .filter((line) => line.startsWith('package:'))
858
+ .map((line) => line.slice('package:'.length).trim())
859
+ return { serial, count: packages.length, packages: packages.slice(0, 200) }
860
+ },
861
+ })
862
+ }
863
+
864
+ function deviceLaunchAppTool() {
865
+ return defineTool({
866
+ name: 'device_launch_app',
867
+ description: 'Launch an installed app by package name (or a unique substring of it). relaunch=true force-stops it first ' +
868
+ 'for a cold start. Resolves the exact package via `pm list packages` so a wrong guess fails loudly instead of ' +
869
+ 'opening the wrong app.',
870
+ parameters: {
871
+ serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
872
+ package: { type: 'string', description: 'Package name or a unique substring of it.' },
873
+ relaunch: { type: 'boolean', description: 'Force-stop the app first (cold start).' },
874
+ },
875
+ output: {
876
+ schema: {
877
+ type: 'object',
878
+ additionalProperties: false,
879
+ properties: {
880
+ serial: { type: 'string', required: true },
881
+ package: { type: 'string', required: true },
882
+ launched: { type: 'boolean', required: true },
883
+ },
884
+ },
885
+ render: (_args, value) => [{ type: 'text', text: `Launched ${value?.package} on ${value?.serial}` }],
886
+ },
887
+ async execute(args) {
888
+ const serial = await requireAndroidDevice(args.serial)
889
+ let pkg = String(args.package ?? '').trim()
890
+ if (pkg === '') throw new Error('device_launch_app requires a package name (or a unique substring).')
891
+ const listOut = await DeviceBuild.capture(DeviceBuild.adb(), ['-s', serial, 'shell', 'pm', 'list', 'packages'])
892
+ const all = listOut
893
+ .split(/\r?\n/)
894
+ .map((line) => line.trim())
895
+ .filter((line) => line.startsWith('package:'))
896
+ .map((line) => line.slice('package:'.length).trim())
897
+ if (!all.includes(pkg)) {
898
+ const matches = all.filter((item) => item.toLowerCase().includes(pkg.toLowerCase()))
899
+ if (matches.length === 0) throw new Error(`No installed package matches "${pkg}". Run device_apps to list them.`)
900
+ if (matches.length > 1) throw new Error(`"${pkg}" matches ${matches.length} packages (${matches.slice(0, 8).join(', ')}${matches.length > 8 ? ', …' : ''}) — be more specific.`)
901
+ pkg = matches[0]
902
+ }
903
+ if (args.relaunch) await DeviceBuild.exec(DeviceBuild.adb(), ['-s', serial, 'shell', 'am', 'force-stop', pkg]).exit
904
+ const code = await DeviceBuild.exec(DeviceBuild.adb(), ['-s', serial, 'shell', 'monkey', '-p', pkg, '-c', 'android.intent.category.LAUNCHER', '1']).exit
905
+ if (code !== 0) throw new Error(`Could not launch ${pkg} (no launcher activity, or monkey failed with exit ${code}).`)
906
+ return { serial, package: pkg, launched: true }
907
+ },
908
+ })
909
+ }
910
+
604
911
  function deviceScreenTool(engine) {
605
912
  return defineTool({
606
913
  name: 'device_screen',
@@ -715,7 +1022,9 @@ function deviceScreenTool(engine) {
715
1022
 
716
1023
  async function captureScreenSize(serial) {
717
1024
  const output = await DeviceBuild.capture(DeviceBuild.adb(), ['-s', serial, 'shell', 'wm', 'size'])
718
- const match = /Physical size:\s*(\d+)x(\d+)/.exec(output)
1025
+ // An `wm size` override wins over the physical panel — the input space is the override.
1026
+ const override = /Override size:\s*(\d+)x(\d+)/.exec(output)
1027
+ const match = override ?? /Physical size:\s*(\d+)x(\d+)/.exec(output)
719
1028
  return match ? { width: Number(match[1]), height: Number(match[2]) } : undefined
720
1029
  }
721
1030
 
@@ -1042,8 +1351,15 @@ function guidance() {
1042
1351
  ' text, and pixel bounds. Narrow with filter/max_depth; on a textless surface (WebView, Compose, canvas) use device_screen.',
1043
1352
  '- device_tap_element: tap a control by resource_id or text/content-desc, with one-call verification via',
1044
1353
  ' expect_text / expect_gone (no separate screenshot needed to know the tap landed).',
1354
+ '- device_wait_for: wait for text to appear/disappear (polls the UI tree, falls back to OCR on textless surfaces);',
1355
+ ' a timeout is a normal matched:false result, never an error. One call replaces an agent-side poll loop.',
1045
1356
  '- device_input: tap/swipe/type/press on the attached Android device at ABSOLUTE pixel coordinates (take the center of a',
1046
- ' device_screen box: x=(x1+x2)/2, y=(y1+y2)/2). The control loop is device_screendevice_input device_screen.',
1357
+ ' device_screen box: x=(x1+x2)/2, y=(y1+y2)/2). The control loop is device_ui_treedevice_tap_element, falling back to',
1358
+ ' device_screen → device_input when a surface exposes no accessibility tree. Typing is ASCII over plain adb; non-ASCII',
1359
+ ' (CJK, emoji) goes through the ADBKeyboard IME when installed, and is refused with the install hint otherwise.',
1360
+ '- device_action: notifications / quick_settings / collapse / lock / wake / assistant / rotate.',
1361
+ '- device_boot / device_shutdown: boot an AVD by name and wait for boot / shut an emulator down (refuses physical devices).',
1362
+ '- device_apps / device_launch_app: list installed packages (never guess a package name) / launch one by package or unique substring.',
1047
1363
  '- device_log: read device logs (logcat main/crash/events, kernel dmesg). Call it when a run fails or an app misbehaves.',
1048
1364
  '- device_status: one normalized snapshot of attached devices, AVDs, running/parked projects, Metro and preview servers.',
1049
1365
  '',
@@ -1113,6 +1429,12 @@ export function apply(ctx, config) {
1113
1429
  deviceScreenTool(engine),
1114
1430
  deviceUiTreeTool(),
1115
1431
  deviceTapElementTool(),
1432
+ deviceWaitForTool(),
1433
+ deviceBootTool(),
1434
+ deviceShutdownTool(),
1435
+ deviceActionTool(),
1436
+ deviceAppsTool(),
1437
+ deviceLaunchAppTool(),
1116
1438
  deviceLogTool(engine),
1117
1439
  deviceStatusTool(engine),
1118
1440
  deviceInputTool(),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-mobilecode",
3
3
  "description": "MobileCode for the dsh web GUI: detect iOS/Android projects, run serve-sim / serve-avd preview servers, and build-install-launch the app on the simulator or emulator from the session — plus agent tools (device_run, device_detect). Hot-pluggable — mounted via the profile bundle list + cordis.patch.yml, no dsh source changes.",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {