dsh-mobilecode 0.4.0 → 0.6.0
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 +57 -1
- package/lib/android-stream.js +3 -4
- package/lib/client.js +1054 -961
- package/lib/device-build.js +255 -18
- package/lib/index.js +559 -18
- package/lib/uitree.js +4 -4
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import * as Vision from './vision.js'
|
|
|
25
25
|
import { DevicePreviewEngine } from './device-preview.js'
|
|
26
26
|
import * as Setup from './setup.js'
|
|
27
27
|
import { registerMobileSkill } from './skill.js'
|
|
28
|
+
import { existsSync } from 'node:fs'
|
|
28
29
|
|
|
29
30
|
export const name = 'mobilecode'
|
|
30
31
|
export const inject = ['webServer', 'tools', 'systemPrompt']
|
|
@@ -220,6 +221,74 @@ function makeRoutes(engine, config, stream) {
|
|
|
220
221
|
writeJson(res, 200, { ok: true })
|
|
221
222
|
},
|
|
222
223
|
},
|
|
224
|
+
// GET /api/dsh-mobilecode/connection → {adb, devices:[{serial, state, model?, wifi}]} — connection card data.
|
|
225
|
+
{
|
|
226
|
+
kind: 'exact',
|
|
227
|
+
path: API_BASE + '/connection',
|
|
228
|
+
handler: async (req, res) => {
|
|
229
|
+
if (!guard(req, res)) return
|
|
230
|
+
if ((req.method ?? 'GET') !== 'GET') { writeJson(res, 405, { error: 'method not allowed' }); return }
|
|
231
|
+
try {
|
|
232
|
+
writeJson(res, 200, { adb: adbHostInfo(), devices: await connectionDevices() })
|
|
233
|
+
} catch (error) {
|
|
234
|
+
writeJson(res, 500, { error: error instanceof Error ? error.message : String(error) })
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
// POST /api/dsh-mobilecode/connect {host, port?, pairing_code?, pairing_port?} — attach a Wi-Fi device (with optional pairing).
|
|
239
|
+
{
|
|
240
|
+
kind: 'exact',
|
|
241
|
+
path: API_BASE + '/connect',
|
|
242
|
+
handler: async (req, res) => {
|
|
243
|
+
if (!fence(req, res, true) || !isPost(req, res)) return
|
|
244
|
+
const body = await readBody(req, res)
|
|
245
|
+
if (body === undefined) return
|
|
246
|
+
try {
|
|
247
|
+
const host = String(body.host ?? '').trim().replace(/^adb:\/\//, '')
|
|
248
|
+
if (host === '') { writeJson(res, 400, { error: 'host is required' }); return }
|
|
249
|
+
const port = body.port ?? 5555
|
|
250
|
+
const target = `${host}:${port}`
|
|
251
|
+
let paired
|
|
252
|
+
const code = String(body.pairing_code ?? '').trim()
|
|
253
|
+
if (code !== '') {
|
|
254
|
+
const pairPort = body.pairing_port ?? 37000
|
|
255
|
+
await DeviceBuild.adbPair(`${host}:${pairPort}`, code)
|
|
256
|
+
paired = true
|
|
257
|
+
}
|
|
258
|
+
const connected = await DeviceBuild.adbConnectSerial(target)
|
|
259
|
+
if (!connected) { writeJson(res, 409, { error: `adb connect ${target} failed — check the Wi-Fi IP/port and Wireless debugging; pass pairing_code + pairing_port if it needs pairing.` }); return }
|
|
260
|
+
writeJson(res, 200, { ok: true, serial: target, paired: paired === true })
|
|
261
|
+
} catch (error) {
|
|
262
|
+
writeJson(res, 502, { error: error instanceof Error ? error.message : String(error) })
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
// POST /api/dsh-mobilecode/pair-qr {timeout_ms?} — WIFI:T:ADB QR + mDNS auto-pair + connect (one blocking call).
|
|
267
|
+
{
|
|
268
|
+
kind: 'exact',
|
|
269
|
+
path: API_BASE + '/pair-qr',
|
|
270
|
+
handler: async (req, res) => {
|
|
271
|
+
if (!fence(req, res, true) || !isPost(req, res)) return
|
|
272
|
+
const body = await readBody(req, res)
|
|
273
|
+
if (body === undefined) return
|
|
274
|
+
try {
|
|
275
|
+
const timeoutMs = Math.min(Math.max(Number(body.timeout_ms) || 60_000, 10_000), 180_000)
|
|
276
|
+
const { name, password } = DeviceBuild.randomQrCredentials()
|
|
277
|
+
const qrText = DeviceBuild.generateQrAdbWifi(name, password)
|
|
278
|
+
const how = 'Render the returned qr_text as a QR code and scan it with the phone (Settings → Connected devices → Pair by QR). Pairing + connect happen automatically once scanned.'
|
|
279
|
+
const pairingSerial = await DeviceBuild.waitForMdnsPairing(timeoutMs)
|
|
280
|
+
if (pairingSerial === undefined) { writeJson(res, 200, { qr_text: qrText, name, password, status: 'timeout', how }); return }
|
|
281
|
+
await DeviceBuild.adbPair(pairingSerial, password)
|
|
282
|
+
const connectHost = pairingSerial.split(':')[0]
|
|
283
|
+
const connected = await DeviceBuild.adbConnectSerial(`${connectHost}:5555`)
|
|
284
|
+
writeJson(res, 200, connected
|
|
285
|
+
? { qr_text: qrText, name, password, status: 'connected', serial: `${connectHost}:5555`, how }
|
|
286
|
+
: { qr_text: qrText, name, password, status: 'paired-not-connected', serial: pairingSerial, how })
|
|
287
|
+
} catch (error) {
|
|
288
|
+
writeJson(res, 502, { error: error instanceof Error ? error.message : String(error) })
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
},
|
|
223
292
|
// GET /api/dsh-mobilecode/doctor → [{name, ok, detail, fix?}] — plugin health check.
|
|
224
293
|
{
|
|
225
294
|
kind: 'exact',
|
|
@@ -728,11 +797,10 @@ function deviceInputTool() {
|
|
|
728
797
|
async execute(args) {
|
|
729
798
|
const serial = await requireAndroidDevice(args.serial)
|
|
730
799
|
const action = args.action ?? 'tap'
|
|
731
|
-
const adbArgs = (shell) => ['-s', serial, 'shell', ...shell]
|
|
732
800
|
switch (action) {
|
|
733
801
|
case 'tap': {
|
|
734
802
|
if (typeof args.x !== 'number' || typeof args.y !== 'number') throw new Error('action=tap requires x and y (integer pixels).')
|
|
735
|
-
await DeviceBuild.
|
|
803
|
+
await DeviceBuild.adbRun(serial, ['shell', 'input', 'tap', String(args.x), String(args.y)])
|
|
736
804
|
return { serial, action, sent: `tap ${args.x},${args.y}` }
|
|
737
805
|
}
|
|
738
806
|
case 'swipe': {
|
|
@@ -740,13 +808,13 @@ function deviceInputTool() {
|
|
|
740
808
|
throw new Error('action=swipe requires x, y, x2, y2.')
|
|
741
809
|
}
|
|
742
810
|
const duration = args.duration ?? 200
|
|
743
|
-
await DeviceBuild.
|
|
811
|
+
await DeviceBuild.adbRun(serial, ['shell', 'input', 'swipe', String(args.x), String(args.y), String(args.x2), String(args.y2), String(duration)])
|
|
744
812
|
return { serial, action, sent: `swipe ${args.x},${args.y}→${args.x2},${args.y2} (${duration}ms)` }
|
|
745
813
|
}
|
|
746
814
|
case 'text': {
|
|
747
815
|
if (typeof args.text !== 'string' || args.text.length === 0) throw new Error('action=text requires a non-empty text string.')
|
|
748
816
|
if (DeviceBuild.isAsciiInput(args.text)) {
|
|
749
|
-
await DeviceBuild.
|
|
817
|
+
await DeviceBuild.adbRun(serial, ['shell', 'input', 'text', DeviceBuild.escapeInputText(args.text)])
|
|
750
818
|
return { serial, action, sent: `text "${args.text}"` }
|
|
751
819
|
}
|
|
752
820
|
// Non-ASCII (CJK, emoji, accented) cannot go through `input text`; the
|
|
@@ -765,7 +833,7 @@ function deviceInputTool() {
|
|
|
765
833
|
const raw = String(args.key ?? '')
|
|
766
834
|
const code = /^\d+$/.test(raw) ? Number(raw) : KEYCODES[raw.toLowerCase()]
|
|
767
835
|
if (!code) throw new Error(`unknown key "${raw}" — use a name from ${Object.keys(KEYCODES).join(', ')} or a raw keycode integer.`)
|
|
768
|
-
await DeviceBuild.
|
|
836
|
+
await DeviceBuild.adbRun(serial, ['shell', 'input', 'keyevent', String(code)])
|
|
769
837
|
return { serial, action, sent: `key ${raw} (${code})` }
|
|
770
838
|
}
|
|
771
839
|
default:
|
|
@@ -775,7 +843,71 @@ function deviceInputTool() {
|
|
|
775
843
|
})
|
|
776
844
|
}
|
|
777
845
|
|
|
778
|
-
/**
|
|
846
|
+
/** Plain-device adb diagnostics (no serial) for the settings Connection card. */
|
|
847
|
+
export function adbHostInfo() {
|
|
848
|
+
return DeviceBuild.adb()
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/** Lines of `adb devices -l` after the header rows, parsed for the settings Connection card. */
|
|
852
|
+
export async function connectionDevices() {
|
|
853
|
+
const output = await DeviceBuild.capture(DeviceBuild.adb(), ["devices", "-l"])
|
|
854
|
+
return parseDeviceLongList(output)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Parse `adb devices -l` into [{serial, state, model, wifi}]. */
|
|
858
|
+
export function parseDeviceLongList(output) {
|
|
859
|
+
return String(output)
|
|
860
|
+
.split(/\r?\n/)
|
|
861
|
+
.map((line) => line.trim())
|
|
862
|
+
.filter((line) => line && !/^List of devices/i.test(line) && !/^\* daemon/i.test(line))
|
|
863
|
+
.map((line) => {
|
|
864
|
+
const [serial, state, ...rest] = line.split(/\s+/)
|
|
865
|
+
const model = /model:(\S+)/.exec(rest.join(" "))?.[1]
|
|
866
|
+
const row = { serial, state: state || "unknown", wifi: DeviceBuild.isWifiSerial(serial) }
|
|
867
|
+
if (model) row.model = model
|
|
868
|
+
return row
|
|
869
|
+
})
|
|
870
|
+
.filter((row) => row.serial)
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function deviceIntentTool() {
|
|
874
|
+
return defineTool({
|
|
875
|
+
name: 'device_intent',
|
|
876
|
+
description: 'Open anything on the device by Android intent: an action (e.g. android.settings.WIFI_SETTINGS), ' +
|
|
877
|
+
'a deep-link URI, or an explicit component (package/.Activity). Reaches screens no tap can address — deep ' +
|
|
878
|
+
'settings pages, app deep links, files. Values are device-shell quoted so metacharacters stay inert.',
|
|
879
|
+
parameters: {
|
|
880
|
+
serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
|
|
881
|
+
action: { type: 'string', description: 'Intent action, e.g. android.settings.WIFI_SETTINGS or android.intent.action.VIEW.' },
|
|
882
|
+
uri: { type: 'string', description: 'Data URI the intent carries, e.g. geo:0,0?q=Berlin or a https:// deep link.' },
|
|
883
|
+
component: { type: 'string', description: 'Explicit component, e.g. com.android.settings/.Settings.' },
|
|
884
|
+
},
|
|
885
|
+
output: {
|
|
886
|
+
schema: {
|
|
887
|
+
type: 'object',
|
|
888
|
+
additionalProperties: false,
|
|
889
|
+
properties: {
|
|
890
|
+
serial: { type: 'string', required: true },
|
|
891
|
+
intent: { type: 'string', required: true },
|
|
892
|
+
started: { type: 'boolean', required: true },
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
render: (_args, value) => [{ type: 'text', text: `Started ${value?.intent} on ${value?.serial}` }],
|
|
896
|
+
},
|
|
897
|
+
async execute(args) {
|
|
898
|
+
const serial = await requireAndroidDevice(args.serial)
|
|
899
|
+
const argv = DeviceBuild.adbIntentArgs({ action: args.action, uri: args.uri, component: args.component })
|
|
900
|
+
if (argv.length === 3) throw new Error('device_intent needs at least one of action, uri or component.')
|
|
901
|
+
const output = await DeviceBuild.adbRun(serial, argv)
|
|
902
|
+
if (/^Error/i.test(output.trim())) throw new Error(`am start refused the intent on ${serial}: ${output.trim().split(/\r?\n/)[0]}`)
|
|
903
|
+
const label = [["-a", args.action], ["-d", args.uri], ["-n", args.component]]
|
|
904
|
+
.filter(([, value]) => value !== undefined && value !== '')
|
|
905
|
+
.map(([flag, value]) => `${flag} ${value}`)
|
|
906
|
+
.join(' ')
|
|
907
|
+
return { serial, intent: label, started: true }
|
|
908
|
+
},
|
|
909
|
+
})
|
|
910
|
+
}
|
|
779
911
|
async function ocrHasText(serial, wantedLower) {
|
|
780
912
|
if (!DeviceBuild.ocrPython()) return undefined
|
|
781
913
|
const png = await DeviceBuild.screenCapture(serial)
|
|
@@ -850,6 +982,392 @@ function deviceWaitForTool() {
|
|
|
850
982
|
})
|
|
851
983
|
}
|
|
852
984
|
|
|
985
|
+
/** First node matching the tap-element selector (exact, then contains), or undefined. Used by device_scroll_to. */
|
|
986
|
+
function findMatchingNode(roots, selector) {
|
|
987
|
+
const identifier = selector.identifier !== undefined && selector.identifier.trim() !== '' ? selector.identifier.trim() : undefined
|
|
988
|
+
const label = selector.label !== undefined && selector.label.trim() !== '' ? selector.label.trim() : undefined
|
|
989
|
+
if (identifier === undefined && label === undefined) return undefined
|
|
990
|
+
const matchesValue = (actual, wanted, mode) => actual !== undefined && (mode === 'exact' ? actual === wanted : actual.toLowerCase().includes(wanted.toLowerCase()))
|
|
991
|
+
const matchesNode = (node, mode) => {
|
|
992
|
+
if (identifier !== undefined && !matchesValue(node.resourceId, identifier, mode)) return false
|
|
993
|
+
if (label !== undefined && !matchesValue(node.text, label, mode) && !matchesValue(node.contentDesc, label, mode)) return false
|
|
994
|
+
return true
|
|
995
|
+
}
|
|
996
|
+
const flat = UiTree.flattenNodes(roots)
|
|
997
|
+
return flat.find((node) => matchesNode(node, 'exact')) ?? flat.find((node) => matchesNode(node, 'contains'))
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function deviceScrollToTool() {
|
|
1001
|
+
return defineTool({
|
|
1002
|
+
name: 'device_scroll_to',
|
|
1003
|
+
description: 'Scroll until an element (by resource_id or text/content-desc, exact then substring) comes into view, then ' +
|
|
1004
|
+
'tap nothing — just report it. Repeats: read UI tree → check the selector → swipe up (or left) until found or max_swipes ' +
|
|
1005
|
+
'exhausted. Completes the control loop for long lists: run device_wait_for won\'t scroll, this brings the element to the ' +
|
|
1006
|
+
'screen so a follow-up device_tap_element can hit it. Returns the found node and how many swipes it took.',
|
|
1007
|
+
parameters: {
|
|
1008
|
+
serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
|
|
1009
|
+
resource_id: { type: 'string', description: 'The resource-id to find, e.g. com.android.settings:id/some_item.' },
|
|
1010
|
+
text: { type: 'string', description: 'Text or content-desc to find (exact wins; falls back to substring).' },
|
|
1011
|
+
max_swipes: { type: 'integer', description: 'Swipe budget before giving up (default 8).' },
|
|
1012
|
+
direction: { type: 'string', enum: ['up', 'left'], description: 'Which way to swipe (default up).' },
|
|
1013
|
+
},
|
|
1014
|
+
output: {
|
|
1015
|
+
schema: {
|
|
1016
|
+
type: 'object',
|
|
1017
|
+
additionalProperties: false,
|
|
1018
|
+
properties: {
|
|
1019
|
+
serial: { type: 'string', required: true },
|
|
1020
|
+
found: { type: 'boolean', required: true },
|
|
1021
|
+
swipes: { type: 'integer', required: true },
|
|
1022
|
+
node: { type: 'string' },
|
|
1023
|
+
bounds: { type: 'array', items: { type: 'integer' } },
|
|
1024
|
+
x: { type: 'integer' },
|
|
1025
|
+
y: { type: 'integer' },
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
render: (_args, value) => {
|
|
1029
|
+
const v = value ?? { serial: '', found: false, swipes: 0 }
|
|
1030
|
+
return [{ type: 'text', text: v.found
|
|
1031
|
+
? `Found ${v.node} at ${v.bounds?.join(',') ?? ''} (center ${v.x},${v.y}) after ${v.swipes} swipe(s) on ${v.serial} — device_tap_element it (text/resource_id) or tap ${v.x},${v.y} via device_input`
|
|
1032
|
+
: `Not found after ${v.swipes} swipes on ${v.serial} — the element may live behind a deeper navigation, or the selector is wrong (run device_ui_tree / device_screen to check).` }]
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
async execute(args) {
|
|
1036
|
+
const serial = await requireAndroidDevice(args.serial)
|
|
1037
|
+
const selector = { identifier: args.resource_id, label: args.text }
|
|
1038
|
+
const maxSwipes = Math.min(Math.max(args.max_swipes ?? 8, 1), 20)
|
|
1039
|
+
const direction = args.direction === 'left' ? 'left' : 'up'
|
|
1040
|
+
// Screen size drives the swipe path so it works on any panel.
|
|
1041
|
+
let swipes = 0
|
|
1042
|
+
for (;;) {
|
|
1043
|
+
const parsed = await UiTree.readUiTree(serial)
|
|
1044
|
+
const found = findMatchingNode(parsed.roots, selector)
|
|
1045
|
+
if (found) {
|
|
1046
|
+
const center = UiTree.boundsCenter(found.bounds)
|
|
1047
|
+
return { serial, found: true, swipes, node: found.text || found.contentDesc || found.resourceId || found.type, bounds: [found.bounds.x, found.bounds.y, found.bounds.w, found.bounds.h], x: center.x, y: center.y }
|
|
1048
|
+
}
|
|
1049
|
+
if (swipes >= maxSwipes) return { serial, found: false, swipes }
|
|
1050
|
+
const sizeStr = await DeviceBuild.adbRun(serial, ['shell', 'wm', 'size']).catch(() => '')
|
|
1051
|
+
const override = /Override size:\s*(\d+)x(\d+)/.exec(sizeStr)
|
|
1052
|
+
const physical = /Physical size:\s*(\d+)x(\d+)/.exec(sizeStr)
|
|
1053
|
+
const match = override ?? physical
|
|
1054
|
+
if (!match) throw new Error(`cannot read the screen size of ${serial} (wm size returned nothing) — cannot swipe`)
|
|
1055
|
+
const w = Number(match[1]); const h = Number(match[2])
|
|
1056
|
+
const fromX = Math.round(w / 2); const fromY = Math.round(h * 0.7); const toY = Math.round(h * 0.3)
|
|
1057
|
+
if (direction === 'left') {
|
|
1058
|
+
await DeviceBuild.adbRun(serial, ['shell', 'input', 'swipe', String(Math.round(w * 0.8)), String(Math.round(h / 2)), String(Math.round(w * 0.2)), String(Math.round(h / 2)), '400'])
|
|
1059
|
+
} else {
|
|
1060
|
+
await DeviceBuild.adbRun(serial, ['shell', 'input', 'swipe', String(fromX), String(fromY), String(fromX), String(toY), '400'])
|
|
1061
|
+
}
|
|
1062
|
+
swipes += 1
|
|
1063
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
1064
|
+
}
|
|
1065
|
+
},
|
|
1066
|
+
})
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function deviceConnectTool() {
|
|
1070
|
+
return defineTool({
|
|
1071
|
+
name: 'device_connect',
|
|
1072
|
+
description: 'Attach an Android device over Wi-Fi: `adb connect <host>:<port>` (requires the phone\'s Wireless debugging' +
|
|
1073
|
+
' "Pair device" flow the first time — pass pairing_code to pair first). Reattaching a known wireless device needs no' +
|
|
1074
|
+
' code. Returns the connected serial.',
|
|
1075
|
+
parameters: {
|
|
1076
|
+
host: { type: 'string', description: 'IP address (or mDNS name) of the device.' },
|
|
1077
|
+
port: { type: 'integer', description: 'Connect port (default 5555).' },
|
|
1078
|
+
pairing_code: { type: 'string', description: 'Six-digit pairing code shown by "Wireless debugging → Pair device".' },
|
|
1079
|
+
pairing_port: { type: 'integer', description: 'Pairing port shown by the pairing dialog (default 37000).' },
|
|
1080
|
+
},
|
|
1081
|
+
output: {
|
|
1082
|
+
schema: {
|
|
1083
|
+
type: 'object',
|
|
1084
|
+
additionalProperties: false,
|
|
1085
|
+
properties: {
|
|
1086
|
+
serial: { type: 'string', required: true },
|
|
1087
|
+
paired: { type: 'boolean' },
|
|
1088
|
+
connected: { type: 'boolean', required: true },
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
render: (_args, value) => {
|
|
1092
|
+
const v = value ?? { serial: '', connected: false }
|
|
1093
|
+
return [{ type: 'text', text: `${v.connected ? 'Connected' : 'Failed to connect'} to ${v.serial}${v.paired ? ' (paired)' : ''}` }]
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
async execute(args) {
|
|
1097
|
+
const host = String(args.host ?? '').trim().replace(/^adb:\/\//, '')
|
|
1098
|
+
if (host === '') throw new Error('device_connect requires a host (IP address).')
|
|
1099
|
+
const port = args.port ?? 5555
|
|
1100
|
+
const target = `${host}:${port}`
|
|
1101
|
+
let paired
|
|
1102
|
+
const code = String(args.pairing_code ?? '').trim()
|
|
1103
|
+
if (code !== '') {
|
|
1104
|
+
const pairPort = args.pairing_port ?? 37000
|
|
1105
|
+
await DeviceBuild.adbPair(`${host}:${pairPort}`, code)
|
|
1106
|
+
paired = true
|
|
1107
|
+
}
|
|
1108
|
+
const connected = await DeviceBuild.adbConnectSerial(target)
|
|
1109
|
+
if (!connected) throw new Error(`adb connect ${target} failed — check the Wi-Fi IP/port and that Wireless debugging is on; if it needs pairing, pass pairing_code + pairing_port.`)
|
|
1110
|
+
return { serial: target, paired, connected: true }
|
|
1111
|
+
},
|
|
1112
|
+
})
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function devicePairQrTool() {
|
|
1116
|
+
return defineTool({
|
|
1117
|
+
name: 'device_pair_qr',
|
|
1118
|
+
description: 'The full Wi-Fi pairing flow: generates a WIFI:T:ADB QR string (render it as a QR code — any QR generator ' +
|
|
1119
|
+
'works — and scan it with the phone: Settings → Connected devices → Pair by QR), waits for the phone to advertise its ' +
|
|
1120
|
+
'pairing service over mDNS (`adb mdns services`, needs adb >= 31), then auto-pairs and connects. One call covers ' +
|
|
1121
|
+
'generate → wait → pair → connect.',
|
|
1122
|
+
parameters: {
|
|
1123
|
+
timeout_ms: { type: 'integer', description: 'How long to wait for the QR scan (default 60000).' },
|
|
1124
|
+
serial: { type: 'string', description: 'Only used to pre-check adb is present; pairing targets whatever the QR scan names.' },
|
|
1125
|
+
},
|
|
1126
|
+
output: {
|
|
1127
|
+
schema: {
|
|
1128
|
+
type: 'object',
|
|
1129
|
+
additionalProperties: false,
|
|
1130
|
+
properties: {
|
|
1131
|
+
qr_text: { type: 'string', required: true },
|
|
1132
|
+
name: { type: 'string', required: true },
|
|
1133
|
+
password: { type: 'string', required: true },
|
|
1134
|
+
status: { type: 'string', required: true },
|
|
1135
|
+
serial: { type: 'string' },
|
|
1136
|
+
how: { type: 'string', required: true },
|
|
1137
|
+
},
|
|
1138
|
+
},
|
|
1139
|
+
render: (_args, value) => {
|
|
1140
|
+
const v = value ?? { qr_text: '', name: '', password: '', status: '' }
|
|
1141
|
+
const lines = [`WIFI:T:ADB QR (status ${v.status})`, ` ${v.qr_text}`, '']
|
|
1142
|
+
if (v.serial) lines.push(`Paired & connected: ${v.serial}`)
|
|
1143
|
+
lines.push('Scan the QR text above with the phone: Settings → Connected devices → Pair by QR. Any QR generator renders it.')
|
|
1144
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
async execute(args) {
|
|
1148
|
+
const timeoutMs = Math.min(Math.max(args.timeout_ms ?? 60_000, 10_000), 180_000)
|
|
1149
|
+
const { name, password } = DeviceBuild.randomQrCredentials()
|
|
1150
|
+
const qrText = DeviceBuild.generateQrAdbWifi(name, password)
|
|
1151
|
+
const how = 'Render the returned qr_text as a QR code and scan it with the phone (Settings → Connected devices → Pair by QR). Pairing + connect happen automatically once scanned.'
|
|
1152
|
+
const pairingSerial = await DeviceBuild.waitForMdnsPairing(timeoutMs)
|
|
1153
|
+
if (pairingSerial === undefined) {
|
|
1154
|
+
return { qr_text: qrText, name, password, status: 'timeout', how }
|
|
1155
|
+
}
|
|
1156
|
+
await DeviceBuild.adbPair(pairingSerial, password)
|
|
1157
|
+
// The pairing dialog usually pairs the tls-port; the connect port is 5555 or mdns-advertised — try 5555.
|
|
1158
|
+
const connectHost = pairingSerial.split(':')[0]
|
|
1159
|
+
const connected = await DeviceBuild.adbConnectSerial(`${connectHost}:5555`)
|
|
1160
|
+
if (!connected) {
|
|
1161
|
+
return { qr_text: qrText, name, password, status: 'paired-not-connected', serial: pairingSerial, how }
|
|
1162
|
+
}
|
|
1163
|
+
return { qr_text: qrText, name, password, status: 'connected', serial: `${connectHost}:5555`, how }
|
|
1164
|
+
},
|
|
1165
|
+
})
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function devicePerfTool() {
|
|
1169
|
+
return defineTool({
|
|
1170
|
+
name: 'device_perf',
|
|
1171
|
+
description: 'One-shot memory / battery / CPU snapshot of an attached Android device (reads /proc/meminfo, dumpsys battery, ' +
|
|
1172
|
+
'/proc/cpuinfo). Use to check the device\'s resource state before or after a run — cheap and deterministic.',
|
|
1173
|
+
parameters: {
|
|
1174
|
+
serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
|
|
1175
|
+
},
|
|
1176
|
+
output: {
|
|
1177
|
+
schema: {
|
|
1178
|
+
type: 'object',
|
|
1179
|
+
additionalProperties: false,
|
|
1180
|
+
properties: {
|
|
1181
|
+
serial: { type: 'string', required: true },
|
|
1182
|
+
memory: { type: 'object', additionalProperties: true },
|
|
1183
|
+
battery: { type: 'object', additionalProperties: true },
|
|
1184
|
+
cpu: { type: 'object', additionalProperties: true },
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
render: (_args, value) => {
|
|
1188
|
+
const v = value ?? { serial: '' }
|
|
1189
|
+
const mem = v.memory ? `RAM ${v.memory.usedPercent}% used (${Math.round(v.memory.usedKb / 1024)}/${Math.round(v.memory.totalKb / 1024)} MB)` : 'RAM n/a'
|
|
1190
|
+
const bat = v.battery && v.battery.level !== undefined ? `Battery ${v.battery.level}% ${v.battery.temperatureC !== undefined ? `(${v.battery.temperatureC}°C) ` : ''}${v.battery.status}` : 'Battery n/a'
|
|
1191
|
+
const cpu = v.cpu && v.cpu.cores ? `CPU ${v.cpu.cores} cores${v.cpu.hardware ? ` (${v.cpu.hardware})` : ''}` : 'CPU n/a'
|
|
1192
|
+
return [{ type: 'text', text: `${v.serial}: ${mem} · ${bat} · ${cpu}` }]
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
async execute(args) {
|
|
1196
|
+
const serial = await requireAndroidDevice(args.serial)
|
|
1197
|
+
const [meminfo, battery, cpuinfo] = await Promise.all([
|
|
1198
|
+
DeviceBuild.adbRun(serial, ['shell', 'cat', '/proc/meminfo']).catch(() => ''),
|
|
1199
|
+
DeviceBuild.adbRun(serial, ['shell', 'dumpsys', 'battery']).catch(() => ''),
|
|
1200
|
+
DeviceBuild.adbRun(serial, ['shell', 'cat', '/proc/cpuinfo']).catch(() => ''),
|
|
1201
|
+
])
|
|
1202
|
+
return {
|
|
1203
|
+
serial,
|
|
1204
|
+
memory: DeviceBuild.parseMeminfo(meminfo),
|
|
1205
|
+
battery: parseBatteryShaped(battery),
|
|
1206
|
+
cpu: DeviceBuild.parseCpuinfo(cpuinfo),
|
|
1207
|
+
}
|
|
1208
|
+
},
|
|
1209
|
+
})
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/** battery status words: dumpsys battery status 2=charging, 5=full etc. */
|
|
1213
|
+
function parseBatteryShaped(output) {
|
|
1214
|
+
const raw = DeviceBuild.parseBattery(output)
|
|
1215
|
+
if (raw === undefined) return undefined
|
|
1216
|
+
const status = { 1: 'unknown', 2: 'charging', 3: 'discharging', 4: 'not charging', 5: 'full' }[raw.status] ?? raw.status
|
|
1217
|
+
const health = { 1: 'unknown', 2: 'good', 3: 'overheat', 4: 'dead', 5: 'over-voltage', 6: 'unspecified', 7: 'cold' }[raw.health] ?? raw.health
|
|
1218
|
+
return { ...raw, status, health }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function deviceAppInfoTool() {
|
|
1222
|
+
return defineTool({
|
|
1223
|
+
name: 'device_app_info',
|
|
1224
|
+
description: 'Detail for one installed app: versionName/versionCode, minSdk/targetSdk, requested permissions, and exported ' +
|
|
1225
|
+
'activities — straight from `dumpsys package <pkg>`. Use to answer "what does this app do / what permissions / which ' +
|
|
1226
|
+
'version" without guessing.',
|
|
1227
|
+
parameters: {
|
|
1228
|
+
package: { type: 'string', description: 'Package name, e.g. com.android.chrome.' },
|
|
1229
|
+
serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
|
|
1230
|
+
},
|
|
1231
|
+
output: {
|
|
1232
|
+
schema: {
|
|
1233
|
+
type: 'object',
|
|
1234
|
+
additionalProperties: false,
|
|
1235
|
+
properties: {
|
|
1236
|
+
package: { type: 'string', required: true },
|
|
1237
|
+
versionName: { type: 'string' },
|
|
1238
|
+
versionCode: { type: 'string' },
|
|
1239
|
+
minSdk: { type: 'string' },
|
|
1240
|
+
targetSdk: { type: 'string' },
|
|
1241
|
+
permissions: { type: 'array', items: { type: 'string' } },
|
|
1242
|
+
activities: { type: 'array', items: { type: 'string' } },
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
render: (_args, value) => {
|
|
1246
|
+
const v = value ?? { package: '' }
|
|
1247
|
+
const lines = [`${v.package}`, ` version ${v.versionName ?? '?'} (${v.versionCode ?? '?'}) · minSdk ${v.minSdk ?? '?'} · targetSdk ${v.targetSdk ?? '?'}`]
|
|
1248
|
+
if (v.permissions?.length) lines.push(` permissions (${v.permissions.length}):`, v.permissions.slice(0, 15).map((p) => ` ${p}`).join('\n'))
|
|
1249
|
+
if (v.activities?.length) lines.push(` activities (${v.activities.length}):`, v.activities.slice(0, 8).map((a) => ` ${a}`).join('\n'))
|
|
1250
|
+
return [{ type: 'text', text: lines.join('\n') }]
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
async execute(args) {
|
|
1254
|
+
const serial = await requireAndroidDevice(args.serial)
|
|
1255
|
+
const pkg = String(args.package ?? '').trim()
|
|
1256
|
+
if (pkg === '') throw new Error('device_app_info requires a package name.')
|
|
1257
|
+
const output = await DeviceBuild.adbRun(serial, ['shell', 'dumpsys', 'package', pkg])
|
|
1258
|
+
const info = DeviceBuild.parseAppInfo(output, pkg)
|
|
1259
|
+
if (info.versionName === undefined) throw new Error(`package ${pkg} is not installed (dumpsys package returned nothing for it) — run device_apps to list what is.`)
|
|
1260
|
+
return info
|
|
1261
|
+
},
|
|
1262
|
+
})
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function deviceInstallTool() {
|
|
1266
|
+
return defineTool({
|
|
1267
|
+
name: 'device_install',
|
|
1268
|
+
description: 'Install a local APK onto an attached Android device (`adb install -r -g`: replace + grant runtime permissions). ' +
|
|
1269
|
+
'The apk_path is a LOCAL path on this host (e.g. a build output). Independent of device_run — use it to sideload a debug ' +
|
|
1270
|
+
'build onto a device whose app is already set up.',
|
|
1271
|
+
parameters: {
|
|
1272
|
+
apk_path: { type: 'string', description: 'Absolute local path to the APK, e.g. F:/app/build/outputs/apk/debug/app-debug.apk.' },
|
|
1273
|
+
serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
|
|
1274
|
+
},
|
|
1275
|
+
output: {
|
|
1276
|
+
schema: {
|
|
1277
|
+
type: 'object',
|
|
1278
|
+
additionalProperties: false,
|
|
1279
|
+
properties: {
|
|
1280
|
+
serial: { type: 'string', required: true },
|
|
1281
|
+
apk: { type: 'string', required: true },
|
|
1282
|
+
installed: { type: 'boolean', required: true },
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
render: (_args, value) => {
|
|
1286
|
+
const v = value ?? { serial: '', apk: '', installed: false }
|
|
1287
|
+
return [{ type: 'text', text: `Installed ${v.apk} on ${v.serial} (${v.installed ? 'ok' : 'failed'})` }]
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
async execute(args) {
|
|
1291
|
+
const serial = await requireAndroidDevice(args.serial)
|
|
1292
|
+
const apk = String(args.apk_path ?? '').trim()
|
|
1293
|
+
if (apk === '') throw new Error('device_install requires apk_path.')
|
|
1294
|
+
if (!existsSync(apk)) throw new Error(`APK not found on this host: ${apk}`)
|
|
1295
|
+
const output = await DeviceBuild.adbRun(serial, ['install', '-r', '-g', apk], { timeoutMs: 180_000 })
|
|
1296
|
+
if (!/success/i.test(output)) throw new Error(`adb install failed on ${serial}: ${output.slice(-300)}`)
|
|
1297
|
+
return { serial, apk, installed: true }
|
|
1298
|
+
},
|
|
1299
|
+
})
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function deviceUninstallTool() {
|
|
1303
|
+
return defineTool({
|
|
1304
|
+
name: 'device_uninstall',
|
|
1305
|
+
description: 'Uninstall an app from an attached Android device (`adb uninstall <pkg>`, keeps data unless -k is wanted). ' +
|
|
1306
|
+
'Irreversible — the app\'s data is removed. Confirm the intent before calling.',
|
|
1307
|
+
parameters: {
|
|
1308
|
+
package: { type: 'string', description: 'Package name to remove, e.g. com.example.app.' },
|
|
1309
|
+
keep_data: { type: 'boolean', description: 'Pass -k to keep the app data (default false).' },
|
|
1310
|
+
serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
|
|
1311
|
+
},
|
|
1312
|
+
output: {
|
|
1313
|
+
schema: {
|
|
1314
|
+
type: 'object',
|
|
1315
|
+
additionalProperties: false,
|
|
1316
|
+
properties: {
|
|
1317
|
+
serial: { type: 'string', required: true },
|
|
1318
|
+
package: { type: 'string', required: true },
|
|
1319
|
+
uninstalled: { type: 'boolean', required: true },
|
|
1320
|
+
},
|
|
1321
|
+
},
|
|
1322
|
+
render: (_args, value) => {
|
|
1323
|
+
const v = value ?? { serial: '', package: '', uninstalled: false }
|
|
1324
|
+
return [{ type: 'text', text: `Uninstalled ${v.package} on ${v.serial} (${v.uninstalled ? 'ok' : 'failed'})` }]
|
|
1325
|
+
},
|
|
1326
|
+
},
|
|
1327
|
+
async execute(args) {
|
|
1328
|
+
const serial = await requireAndroidDevice(args.serial)
|
|
1329
|
+
const pkg = String(args.package ?? '').trim()
|
|
1330
|
+
if (pkg === '') throw new Error('device_uninstall requires a package name.')
|
|
1331
|
+
const argv = args.keep_data === true ? ['uninstall', '-k', pkg] : ['uninstall', pkg]
|
|
1332
|
+
const output = await DeviceBuild.adbRun(serial, argv)
|
|
1333
|
+
if (!/success/i.test(output)) throw new Error(`adb uninstall failed on ${serial}: ${output.slice(-300)}`)
|
|
1334
|
+
return { serial, package: pkg, uninstalled: true }
|
|
1335
|
+
},
|
|
1336
|
+
})
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function deviceRebootTool() {
|
|
1340
|
+
return defineTool({
|
|
1341
|
+
name: 'device_reboot',
|
|
1342
|
+
description: 'Reboot an attached Android device: `adb reboot` (normal), recovery, or bootloader. The device goes offline and ' +
|
|
1343
|
+
'comes back in a minute or two — a subsequent call to a device_* tool will auto-wait or fail with a reconnect hint. ' +
|
|
1344
|
+
'Disruptive: rebooting interrupts anything running on the device.',
|
|
1345
|
+
parameters: {
|
|
1346
|
+
mode: { type: 'string', enum: ['normal', 'recovery', 'bootloader'], description: 'Where to reboot into (default normal).' },
|
|
1347
|
+
serial: { type: 'string', description: 'Android device serial. Omit to use the first attached device.' },
|
|
1348
|
+
},
|
|
1349
|
+
output: {
|
|
1350
|
+
schema: {
|
|
1351
|
+
type: 'object',
|
|
1352
|
+
additionalProperties: false,
|
|
1353
|
+
properties: {
|
|
1354
|
+
serial: { type: 'string', required: true },
|
|
1355
|
+
mode: { type: 'string', required: true },
|
|
1356
|
+
rebooting: { type: 'boolean', required: true },
|
|
1357
|
+
},
|
|
1358
|
+
},
|
|
1359
|
+
render: (_args, value) => [{ type: 'text', text: `Rebooting ${value?.serial} into ${value?.mode}` }],
|
|
1360
|
+
},
|
|
1361
|
+
async execute(args) {
|
|
1362
|
+
const serial = await requireAndroidDevice(args.serial)
|
|
1363
|
+
const mode = args.mode === 'recovery' || args.mode === 'bootloader' ? args.mode : 'normal'
|
|
1364
|
+
const argv = mode === 'normal' ? ['reboot'] : ['reboot', mode]
|
|
1365
|
+
await DeviceBuild.adbRun(serial, argv)
|
|
1366
|
+
return { serial, mode, rebooting: true }
|
|
1367
|
+
},
|
|
1368
|
+
})
|
|
1369
|
+
}
|
|
1370
|
+
|
|
853
1371
|
function deviceBootTool() {
|
|
854
1372
|
return defineTool({
|
|
855
1373
|
name: 'device_boot',
|
|
@@ -931,7 +1449,7 @@ function deviceShutdownTool() {
|
|
|
931
1449
|
if (!isEmulator) {
|
|
932
1450
|
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.`)
|
|
933
1451
|
}
|
|
934
|
-
await DeviceBuild.
|
|
1452
|
+
await DeviceBuild.adbRun(serial, ['emu', 'kill'])
|
|
935
1453
|
return { serial, shutdown: true }
|
|
936
1454
|
},
|
|
937
1455
|
})
|
|
@@ -971,15 +1489,15 @@ function deviceActionTool() {
|
|
|
971
1489
|
const serial = await requireAndroidDevice(args.serial)
|
|
972
1490
|
const action = String(args.action ?? '')
|
|
973
1491
|
if (action === 'rotate') {
|
|
974
|
-
const current = Number(await DeviceBuild.
|
|
1492
|
+
const current = Number(await DeviceBuild.adbRun(serial, ['shell', 'settings', 'get', 'system', 'user_rotation']))
|
|
975
1493
|
const next = ((Number.isFinite(current) ? current : 0) + 1) % 4
|
|
976
|
-
await DeviceBuild.
|
|
977
|
-
await DeviceBuild.
|
|
1494
|
+
await DeviceBuild.adbRun(serial, ['shell', 'settings', 'put', 'system', 'accelerometer_rotation', '0'])
|
|
1495
|
+
await DeviceBuild.adbRun(serial, ['shell', 'settings', 'put', 'system', 'user_rotation', String(next)])
|
|
978
1496
|
return { serial, action, rotation: next }
|
|
979
1497
|
}
|
|
980
1498
|
const shell = DEVICE_ACTIONS[action]
|
|
981
1499
|
if (!shell) throw new Error(`unknown action "${action}" — use ${[...Object.keys(DEVICE_ACTIONS), 'rotate'].join(', ')}.`)
|
|
982
|
-
await DeviceBuild.
|
|
1500
|
+
await DeviceBuild.adbRun(serial, ['shell', ...shell])
|
|
983
1501
|
return { serial, action }
|
|
984
1502
|
},
|
|
985
1503
|
})
|
|
@@ -1012,7 +1530,7 @@ function deviceAppsTool() {
|
|
|
1012
1530
|
},
|
|
1013
1531
|
async execute(args) {
|
|
1014
1532
|
const serial = await requireAndroidDevice(args.serial)
|
|
1015
|
-
const output = await DeviceBuild.
|
|
1533
|
+
const output = await DeviceBuild.adbRun(serial, ['shell', 'pm', 'list', 'packages', ...(args.include_system ? [] : ['-3'])])
|
|
1016
1534
|
const packages = output
|
|
1017
1535
|
.split(/\r?\n/)
|
|
1018
1536
|
.map((line) => line.trim())
|
|
@@ -1050,7 +1568,7 @@ function deviceLaunchAppTool() {
|
|
|
1050
1568
|
const serial = await requireAndroidDevice(args.serial)
|
|
1051
1569
|
let pkg = String(args.package ?? '').trim()
|
|
1052
1570
|
if (pkg === '') throw new Error('device_launch_app requires a package name (or a unique substring).')
|
|
1053
|
-
const listOut = await DeviceBuild.
|
|
1571
|
+
const listOut = await DeviceBuild.adbRun(serial, ['shell', 'pm', 'list', 'packages'])
|
|
1054
1572
|
const all = listOut
|
|
1055
1573
|
.split(/\r?\n/)
|
|
1056
1574
|
.map((line) => line.trim())
|
|
@@ -1062,9 +1580,9 @@ function deviceLaunchAppTool() {
|
|
|
1062
1580
|
if (matches.length > 1) throw new Error(`"${pkg}" matches ${matches.length} packages (${matches.slice(0, 8).join(', ')}${matches.length > 8 ? ', …' : ''}) — be more specific.`)
|
|
1063
1581
|
pkg = matches[0]
|
|
1064
1582
|
}
|
|
1065
|
-
if (args.relaunch) await DeviceBuild.
|
|
1066
|
-
const
|
|
1067
|
-
if (
|
|
1583
|
+
if (args.relaunch) await DeviceBuild.adbRun(serial, ['shell', 'am', 'force-stop', pkg])
|
|
1584
|
+
const launched = await DeviceBuild.adbRun(serial, ['shell', 'monkey', '-p', pkg, '-c', 'android.intent.category.LAUNCHER', '1']).catch((error) => { throw new Error(`Could not launch ${pkg}: ${error instanceof Error ? error.message : error}`) })
|
|
1585
|
+
if (/no activities found|no events to send/i.test(launched)) throw new Error(`Could not launch ${pkg} (no launcher activity).`)
|
|
1068
1586
|
return { serial, package: pkg, launched: true }
|
|
1069
1587
|
},
|
|
1070
1588
|
})
|
|
@@ -1240,7 +1758,7 @@ function deviceScreenTool(engine, vision) {
|
|
|
1240
1758
|
}
|
|
1241
1759
|
|
|
1242
1760
|
async function captureScreenSize(serial) {
|
|
1243
|
-
const output = await DeviceBuild.
|
|
1761
|
+
const output = await DeviceBuild.adbRun(serial, ['shell', 'wm', 'size'])
|
|
1244
1762
|
// An `wm size` override wins over the physical panel — the input space is the override.
|
|
1245
1763
|
const override = /Override size:\s*(\d+)x(\d+)/.exec(output)
|
|
1246
1764
|
const match = override ?? /Physical size:\s*(\d+)x(\d+)/.exec(output)
|
|
@@ -1362,7 +1880,7 @@ function deviceTapElementTool() {
|
|
|
1362
1880
|
const selector = { identifier: args.resource_id, label: args.text }
|
|
1363
1881
|
const { node, matchedBy } = UiTree.resolveTapTarget(parsed.roots, selector, { tool: 'device_tap_element', allowOffscreen: args.allow_offscreen === true })
|
|
1364
1882
|
const center = UiTree.boundsCenter(node.bounds)
|
|
1365
|
-
await DeviceBuild.
|
|
1883
|
+
await DeviceBuild.adbRun(serial, ['shell', 'input', 'tap', String(center.x), String(center.y)])
|
|
1366
1884
|
const describe = () => {
|
|
1367
1885
|
const parts = []
|
|
1368
1886
|
if (node.resourceId) parts.push(`resource_id ${node.resourceId}`)
|
|
@@ -1572,6 +2090,16 @@ function guidance() {
|
|
|
1572
2090
|
' expect_text / expect_gone (no separate screenshot needed to know the tap landed).',
|
|
1573
2091
|
'- device_wait_for: wait for text to appear/disappear (polls the UI tree, falls back to OCR on textless surfaces);',
|
|
1574
2092
|
' a timeout is a normal matched:false result, never an error. One call replaces an agent-side poll loop.',
|
|
2093
|
+
'- device_scroll_to: scroll until an element (resource_id or text) comes into view, then report it — use before tapping',
|
|
2094
|
+
' elements that are off-screen in long lists (Settings pages, app lists). Returns the found node + its center tap point.',
|
|
2095
|
+
'- device_connect / device_pair_qr: attach a Wi-Fi device. device_connect does `adb connect host:port` (pass pairing_code +',
|
|
2096
|
+
' pairing_port for the first-time "Pair device" flow). device_pair_qr does the full journey: generates a WIFI:T:ADB QR',
|
|
2097
|
+
' string (render it as a QR code and scan it with the phone: Settings → Connected devices → Pair by QR), waits for the',
|
|
2098
|
+
' pairing service over mDNS, then auto-pairs and connects. Needs adb >= 31 for the QR flow.',
|
|
2099
|
+
'- device_perf: one-shot RAM / battery / CPU snapshot of an attached device (meminfo + dumpsys battery + cpuinfo).',
|
|
2100
|
+
'- device_app_info / device_install / device_uninstall: per-app detail (version, SDK, permissions, activities via dumpsys',
|
|
2101
|
+
' package) / sideload a local APK onto a running device (`install -r -g`) / remove an app. Uninstall is destructive.',
|
|
2102
|
+
'- device_reboot: reboot the device (normal / recovery / bootloader). The device drops off then comes back; disruptive.',
|
|
1575
2103
|
'- device_input: tap/swipe/type/press on the attached Android device at ABSOLUTE pixel coordinates (take the center of a',
|
|
1576
2104
|
' device_screen box: x=(x1+x2)/2, y=(y1+y2)/2). The control loop is device_ui_tree → device_tap_element, falling back to',
|
|
1577
2105
|
' device_screen → device_input when a surface exposes no accessibility tree. Typing is ASCII over plain adb; non-ASCII',
|
|
@@ -1579,6 +2107,10 @@ function guidance() {
|
|
|
1579
2107
|
'- device_action: notifications / quick_settings / collapse / lock / wake / assistant / rotate.',
|
|
1580
2108
|
'- device_boot / device_shutdown: boot an AVD by name and wait for boot / shut an emulator down (refuses physical devices).',
|
|
1581
2109
|
'- device_apps / device_launch_app: list installed packages (never guess a package name) / launch one by package or unique substring.',
|
|
2110
|
+
'- device_intent: open anything by Android intent (action, deep-link URI, or package/.Activity). Reaches screens no tap can address.',
|
|
2111
|
+
' Every adb command runs through one classified boundary: a dropped Wi-Fi connection (ip:port serial) gets exactly one',
|
|
2112
|
+
' `adb connect` attempt, read-only commands then replay automatically, and side-effectful ones refuse the replay —',
|
|
2113
|
+
' a replayed tap could double-tap — and raise "reconnected — call again" instead.',
|
|
1582
2114
|
'- device_stream: drive the live screen stream the Devices panel shows (status / start an online device / stop). Agents',
|
|
1583
2115
|
' that just need to see the screen should prefer device_screen or device_ui_tree.',
|
|
1584
2116
|
'- device_log: read device logs (logcat main/crash/events, kernel dmesg). Call it when a run fails or an app misbehaves.',
|
|
@@ -1661,6 +2193,15 @@ export function apply(ctx, config) {
|
|
|
1661
2193
|
deviceActionTool(),
|
|
1662
2194
|
deviceAppsTool(),
|
|
1663
2195
|
deviceLaunchAppTool(),
|
|
2196
|
+
deviceIntentTool(),
|
|
2197
|
+
deviceScrollToTool(),
|
|
2198
|
+
deviceConnectTool(),
|
|
2199
|
+
devicePairQrTool(),
|
|
2200
|
+
devicePerfTool(),
|
|
2201
|
+
deviceAppInfoTool(),
|
|
2202
|
+
deviceInstallTool(),
|
|
2203
|
+
deviceUninstallTool(),
|
|
2204
|
+
deviceRebootTool(),
|
|
1664
2205
|
deviceStreamTool(streamHost, streamAccess),
|
|
1665
2206
|
deviceLogTool(engine),
|
|
1666
2207
|
deviceStatusTool(engine),
|