@mobileaidev/ai-app-bridge 0.2.5 → 0.2.7

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.
@@ -31,6 +31,8 @@ Commands:
31
31
  network Read generic in-app network records.
32
32
  state Read generic in-app state records.
33
33
  events Read generic in-app event records.
34
+ freeze-app Stop the target app processes with SIGSTOP.
35
+ thaw-app Resume the target app processes with SIGCONT.
34
36
 
35
37
  H5/WebView commands:
36
38
  h5-dom Read native Android WebView DOM.
@@ -48,7 +50,7 @@ Flutter commands:
48
50
  flutter-nodes Read Flutter operable nodes.
49
51
  flutter-action Dispatch a raw Flutter action payload.
50
52
  tap-flutter-text Tap a Flutter node by text.
51
- input-flutter-text Set Flutter text through the Flutter action bridge.
53
+ input-flutter-text Set Flutter text through the Flutter action bridge.
52
54
  scroll-flutter Scroll Flutter content.
53
55
  flutter-h5-dom Read DOM through a Flutter H5 adapter.
54
56
  flutter-h5-eval Execute JavaScript through a Flutter H5 adapter.
@@ -62,18 +64,19 @@ Device/action commands:
62
64
  tap-text Tap a visible node by exact text or content description.
63
65
  tap-uia-text Tap a UIAutomator node by text.
64
66
  wait-text Wait until text appears in bridge or UIAutomator output.
65
- input-text Set native Android text through the in-app bridge; ASCII can fall back to ADB.
67
+ input-text Set native Android text through the in-app bridge; ASCII can fall back to ADB.
66
68
  swipe Swipe device coordinates through ADB.
67
69
  keyevent Send an Android keyevent through ADB.
68
70
  keyboard-state Read Android soft keyboard visibility from dumpsys.
69
71
  hide-keyboard Hide the Android soft keyboard when it is visible.
70
72
 
71
- App/permission commands:
72
- install-apk Install an APK and assist device-side installer screens.
73
- launch-app Launch the target package LAUNCHER Activity.
74
- launch-activity Launch an explicit Android Activity component.
75
- launch-native-test Launch the debug native Android bridge test Activity.
76
- launch-flutter Launch the Flutter Activity.
73
+ App/permission commands:
74
+ install-apk Install an APK and assist device-side installer screens.
75
+ clear-app-data Clear target app local data through the bridge runtime.
76
+ launch-app Launch the target package LAUNCHER Activity.
77
+ launch-activity Launch an explicit Android Activity component.
78
+ launch-native-test Launch the debug native Android bridge test Activity.
79
+ launch-flutter Launch the Flutter Activity.
77
80
  permission-state Read Android runtime permission state.
78
81
  permission-grant Grant an Android runtime permission.
79
82
  permission-revoke Revoke an Android runtime permission.
@@ -94,14 +97,14 @@ Options:
94
97
  --adb-timeout-ms <ms> Timeout for ADB subprocesses.
95
98
  --out-file <path> Screenshot output path.
96
99
  --artifact-dir <path> Directory for generated screenshot/artifact defaults.
97
- --apk-path <path> APK path used by install-apk.
98
- --activity <name> Activity class for launch-activity or launch-app override.
99
- --component <pkg/act> Explicit Android component for launch-activity.
100
- --action <name> Intent action for launch-activity.
101
- --category <name> Intent category for launch-activity; may be repeated.
102
- --data <uri> Intent data URI for launch-activity.
103
- --extra <key=value> String intent extra for launch-activity; may be repeated.
104
- --text <text> Text used by Unicode-safe bridge input commands.
100
+ --apk-path <path> APK path used by install-apk.
101
+ --activity <name> Activity class for launch-activity or launch-app override.
102
+ --component <pkg/act> Explicit Android component for launch-activity.
103
+ --action <name> Intent action for launch-activity.
104
+ --category <name> Intent category for launch-activity; may be repeated.
105
+ --data <uri> Intent data URI for launch-activity.
106
+ --extra <key=value> String intent extra for launch-activity; may be repeated.
107
+ --text <text> Text used by Unicode-safe bridge input commands.
105
108
  --value <text> Text value used by h5-input or flutter-h5-input.
106
109
  --selector <css> CSS selector used by H5 commands.
107
110
  --target-text <text> Text used by tap-text or wait-text.
@@ -155,7 +158,7 @@ Options:
155
158
  --permission <name> Android permission for permission-* commands.
156
159
  --op <name> App-op name for appops-set.
157
160
  --mode <mode> App-op mode for appops-set.
158
- --pid <pid|current> PID filter for logcat.
161
+ --pid <pid|current> PID filter for logcat, or explicit process for freeze/thaw.
159
162
  --app-pid Filter logcat by current app pid.
160
163
  --tag <tag[,tag]> Logcat tag filter.
161
164
  --level <level> Minimum logcat level.
@@ -229,19 +232,19 @@ async function runCommand(command, options, ctx) {
229
232
  }
230
233
  case 'flutter-nodes':
231
234
  return flutterNodes(ctx);
232
- case 'tap-flutter-text':
233
- return flutterAction(ctx, { action: 'tapText', text: requiredString(options.targetText, 'targetText') });
234
- case 'input-flutter-text': {
235
- const result = await flutterAction(ctx, {
236
- action: 'inputText',
237
- text: requiredString(options.text, 'text'),
238
- ...(options.tapX !== undefined && options.tapY !== undefined ? { x: Number(options.tapX), y: Number(options.tapY) } : {}),
239
- });
240
- if (booleanOption(options.hideKeyboard)) {
241
- result.keyboard = await hideKeyboard(ctx, options);
242
- }
243
- return result;
244
- }
235
+ case 'tap-flutter-text':
236
+ return flutterAction(ctx, { action: 'tapText', text: requiredString(options.targetText, 'targetText') });
237
+ case 'input-flutter-text': {
238
+ const result = await flutterAction(ctx, {
239
+ action: 'inputText',
240
+ text: requiredString(options.text, 'text'),
241
+ ...(options.tapX !== undefined && options.tapY !== undefined ? { x: Number(options.tapX), y: Number(options.tapY) } : {}),
242
+ });
243
+ if (booleanOption(options.hideKeyboard)) {
244
+ result.keyboard = await hideKeyboard(ctx, options);
245
+ }
246
+ return result;
247
+ }
245
248
  case 'scroll-flutter':
246
249
  return options.targetText
247
250
  ? flutterAction(ctx, { action: 'scrollUntilText', text: options.targetText, maxSwipes: Number(options.maxSwipes || 12) })
@@ -256,6 +259,10 @@ async function runCommand(command, options, ctx) {
256
259
  return bridgeGet(ctx, withQuery('/v1/state', captureQuery(options)));
257
260
  case 'events':
258
261
  return bridgeGet(ctx, withQuery('/v1/events', captureQuery(options)));
262
+ case 'freeze-app':
263
+ return freezeApp(ctx, options);
264
+ case 'thaw-app':
265
+ return thawApp(ctx, options);
259
266
  case 'h5-dom':
260
267
  return bridgeGet(ctx, '/v1/h5/dom');
261
268
  case 'h5-eval':
@@ -298,6 +305,8 @@ async function runCommand(command, options, ctx) {
298
305
  return hideKeyboard(ctx, options);
299
306
  case 'install-apk':
300
307
  return installApk(ctx, options);
308
+ case 'clear-app-data':
309
+ return clearAppData(ctx);
301
310
  case 'webview-pages':
302
311
  return webviewPages(ctx, options);
303
312
  case 'webview-network':
@@ -327,15 +336,15 @@ async function runCommand(command, options, ctx) {
327
336
  return appopsSet(ctx, requiredString(options.op, 'op'), requiredString(options.mode, 'mode'));
328
337
  case 'tap-uia-text':
329
338
  return tapUiaText(ctx, requiredString(options.targetText, 'targetText'), options);
330
- case 'permission-dialog':
331
- return permissionDialog(ctx, options);
332
- case 'launch-app':
333
- return launchApp(ctx, options);
334
- case 'launch-activity':
335
- return launchActivity(ctx, options);
336
- case 'launch-native-test':
337
- return launchNativeTest(ctx);
338
- case 'launch-flutter':
339
+ case 'permission-dialog':
340
+ return permissionDialog(ctx, options);
341
+ case 'launch-app':
342
+ return launchApp(ctx, options);
343
+ case 'launch-activity':
344
+ return launchActivity(ctx, options);
345
+ case 'launch-native-test':
346
+ return launchNativeTest(ctx);
347
+ case 'launch-flutter':
339
348
  return launchFlutter(ctx, options.initialRoute || '');
340
349
  case 'smoke':
341
350
  return smoke(ctx, options);
@@ -359,25 +368,25 @@ function parseArgs(argv) {
359
368
  const rawName = arg.slice(2);
360
369
  const name = rawName.replace(/-([a-z])/g, (_, value) => value.toUpperCase());
361
370
  const next = argv[index + 1];
362
- if (next === undefined || next.startsWith('--')) {
363
- appendOption(options, name, true);
364
- continue;
365
- }
366
- appendOption(options, name, next);
367
- index += 1;
368
- }
369
- return { command, options };
370
- }
371
-
372
- function appendOption(options, name, value) {
373
- if (name === 'extra' || name === 'category') {
374
- if (!Array.isArray(options[name])) options[name] = [];
375
- options[name].push(value);
376
- return;
377
- }
378
- options[name] = value;
379
- }
380
-
371
+ if (next === undefined || next.startsWith('--')) {
372
+ appendOption(options, name, true);
373
+ continue;
374
+ }
375
+ appendOption(options, name, next);
376
+ index += 1;
377
+ }
378
+ return { command, options };
379
+ }
380
+
381
+ function appendOption(options, name, value) {
382
+ if (name === 'extra' || name === 'category') {
383
+ if (!Array.isArray(options[name])) options[name] = [];
384
+ options[name].push(value);
385
+ return;
386
+ }
387
+ options[name] = value;
388
+ }
389
+
381
390
  async function adb(ctx, args, { binary = false } = {}) {
382
391
  const allArgs = adbArgs(ctx, args);
383
392
  return new Promise((resolve, reject) => {
@@ -782,41 +791,41 @@ async function ensureForward(ctx) {
782
791
  };
783
792
  }
784
793
 
785
- async function resolveDevicePort(ctx) {
786
- if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
787
- try {
788
- const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
789
- const state = JSON.parse(result.stdout.trim());
790
- const discoveredPort = Number(state.port);
791
- if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
792
- return { port: discoveredPort, source: 'package-port-file', state };
793
- }
794
- } catch (error) {
795
- if (!shouldUseDefaultPortFallback(ctx)) {
796
- ctx.devicePortSource = 'package-port-file';
797
- ctx.devicePortError = firstErrorLine(error);
798
- error.aiAppBridgePortDiscovery = true;
799
- throw error;
800
- }
801
- return {
802
- port: ctx.port,
803
- source: 'default-port',
804
- error: firstErrorLine(error),
805
- };
806
- }
807
- if (!shouldUseDefaultPortFallback(ctx)) {
808
- const error = new Error(`bridge port discovery failed for explicit package: ${ctx.packageName}`);
809
- error.aiAppBridgePortDiscovery = true;
810
- ctx.devicePortSource = 'package-port-file';
811
- ctx.devicePortError = firstErrorLine(error);
812
- throw error;
813
- }
814
- return { port: ctx.port, source: 'default-port' };
815
- }
816
-
817
- function shouldUseDefaultPortFallback(ctx) {
818
- return !ctx.explicitPackageName;
819
- }
794
+ async function resolveDevicePort(ctx) {
795
+ if (ctx.explicitPort) return { port: ctx.port, source: 'explicit-port' };
796
+ try {
797
+ const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'cat', 'files/ai_app_bridge_port.json']);
798
+ const state = JSON.parse(result.stdout.trim());
799
+ const discoveredPort = Number(state.port);
800
+ if (state.ok === true && Number.isInteger(discoveredPort) && discoveredPort > 0) {
801
+ return { port: discoveredPort, source: 'package-port-file', state };
802
+ }
803
+ } catch (error) {
804
+ if (!shouldUseDefaultPortFallback(ctx)) {
805
+ ctx.devicePortSource = 'package-port-file';
806
+ ctx.devicePortError = firstErrorLine(error);
807
+ error.aiAppBridgePortDiscovery = true;
808
+ throw error;
809
+ }
810
+ return {
811
+ port: ctx.port,
812
+ source: 'default-port',
813
+ error: firstErrorLine(error),
814
+ };
815
+ }
816
+ if (!shouldUseDefaultPortFallback(ctx)) {
817
+ const error = new Error(`bridge port discovery failed for explicit package: ${ctx.packageName}`);
818
+ error.aiAppBridgePortDiscovery = true;
819
+ ctx.devicePortSource = 'package-port-file';
820
+ ctx.devicePortError = firstErrorLine(error);
821
+ throw error;
822
+ }
823
+ return { port: ctx.port, source: 'default-port' };
824
+ }
825
+
826
+ function shouldUseDefaultPortFallback(ctx) {
827
+ return !ctx.explicitPackageName;
828
+ }
820
829
 
821
830
  async function bridgeStatus(ctx, options = {}) {
822
831
  try {
@@ -969,24 +978,24 @@ function normalizeBridgeError(error) {
969
978
  suggestion: 'Confirm the target app is running and that the resolved bridge port belongs to this package.',
970
979
  };
971
980
  }
972
- if (lower.includes('adb timed out')) {
973
- return {
974
- code: 'adb_timeout',
975
- message,
976
- suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
977
- };
978
- }
979
- if (error?.aiAppBridgePackageMismatch || lower.includes('bridge package mismatch')) {
980
- return {
981
- code: 'bridge_package_mismatch',
982
- message,
983
- suggestion: 'The resolved bridge port belongs to another package. Relaunch the target app and retry with the explicit packageName.',
984
- };
985
- }
986
- if (error?.aiAppBridgePortDiscovery || lower.includes('bridge port discovery failed') || lower.includes('run-as') || lower.includes('package not found')) {
987
- return {
988
- code: 'bridge_port_discovery_failed',
989
- message,
981
+ if (lower.includes('adb timed out')) {
982
+ return {
983
+ code: 'adb_timeout',
984
+ message,
985
+ suggestion: 'Check the device state and retry; if the bridge port is known, pass --port to skip package port discovery.',
986
+ };
987
+ }
988
+ if (error?.aiAppBridgePackageMismatch || lower.includes('bridge package mismatch')) {
989
+ return {
990
+ code: 'bridge_package_mismatch',
991
+ message,
992
+ suggestion: 'The resolved bridge port belongs to another package. Relaunch the target app and retry with the explicit packageName.',
993
+ };
994
+ }
995
+ if (error?.aiAppBridgePortDiscovery || lower.includes('bridge port discovery failed') || lower.includes('run-as') || lower.includes('package not found')) {
996
+ return {
997
+ code: 'bridge_port_discovery_failed',
998
+ message,
990
999
  suggestion: 'Install a debuggable build for the requested package or pass --port explicitly.',
991
1000
  };
992
1001
  }
@@ -1008,38 +1017,38 @@ function firstErrorLine(error) {
1008
1017
  return String(error?.message || error || 'unknown_error').split(/\r?\n/).find(Boolean) || 'unknown_error';
1009
1018
  }
1010
1019
 
1011
- async function bridgeGet(ctx, requestPath) {
1012
- await ensureForward(ctx);
1013
- const body = await httpGet(bridgeUrl(ctx, requestPath));
1014
- const payload = JSON.parse(body);
1015
- verifyBridgeTargetPackage(ctx, payload, requestPath);
1016
- return payload;
1017
- }
1018
-
1019
- async function bridgePost(ctx, requestPath, payload) {
1020
- await ensureForward(ctx);
1021
- const body = await httpPost(bridgeUrl(ctx, requestPath), payload);
1022
- const responsePayload = JSON.parse(body);
1023
- verifyBridgeTargetPackage(ctx, responsePayload, requestPath);
1024
- return responsePayload;
1025
- }
1026
-
1027
- function bridgeUrl(ctx, requestPath) {
1028
- return `http://127.0.0.1:${ctx.hostPort || ctx.port}${requestPath}`;
1029
- }
1030
-
1031
- function verifyBridgeTargetPackage(ctx, payload, requestPath) {
1032
- if (!ctx.explicitPackageName || !payload || typeof payload !== 'object') return;
1033
- const responsePackageName = payload.app && typeof payload.app === 'object'
1034
- ? payload.app.packageName
1035
- : undefined;
1036
- if (!responsePackageName || responsePackageName === ctx.packageName) return;
1037
- const error = new Error(`bridge package mismatch for ${requestPath}: expected ${ctx.packageName}, got ${responsePackageName}`);
1038
- error.aiAppBridgePackageMismatch = true;
1039
- error.expectedPackageName = ctx.packageName;
1040
- error.actualPackageName = responsePackageName;
1041
- throw error;
1042
- }
1020
+ async function bridgeGet(ctx, requestPath) {
1021
+ await ensureForward(ctx);
1022
+ const body = await httpGet(bridgeUrl(ctx, requestPath));
1023
+ const payload = JSON.parse(body);
1024
+ verifyBridgeTargetPackage(ctx, payload, requestPath);
1025
+ return payload;
1026
+ }
1027
+
1028
+ async function bridgePost(ctx, requestPath, payload) {
1029
+ await ensureForward(ctx);
1030
+ const body = await httpPost(bridgeUrl(ctx, requestPath), payload);
1031
+ const responsePayload = JSON.parse(body);
1032
+ verifyBridgeTargetPackage(ctx, responsePayload, requestPath);
1033
+ return responsePayload;
1034
+ }
1035
+
1036
+ function bridgeUrl(ctx, requestPath) {
1037
+ return `http://127.0.0.1:${ctx.hostPort || ctx.port}${requestPath}`;
1038
+ }
1039
+
1040
+ function verifyBridgeTargetPackage(ctx, payload, requestPath) {
1041
+ if (!ctx.explicitPackageName || !payload || typeof payload !== 'object') return;
1042
+ const responsePackageName = payload.app && typeof payload.app === 'object'
1043
+ ? payload.app.packageName
1044
+ : undefined;
1045
+ if (!responsePackageName || responsePackageName === ctx.packageName) return;
1046
+ const error = new Error(`bridge package mismatch for ${requestPath}: expected ${ctx.packageName}, got ${responsePackageName}`);
1047
+ error.aiAppBridgePackageMismatch = true;
1048
+ error.expectedPackageName = ctx.packageName;
1049
+ error.actualPackageName = responsePackageName;
1050
+ throw error;
1051
+ }
1043
1052
 
1044
1053
  function httpGet(url) {
1045
1054
  return new Promise((resolve, reject) => {
@@ -1528,6 +1537,95 @@ async function packagePidsFor(ctx) {
1528
1537
  }
1529
1538
  }
1530
1539
 
1540
+ async function freezeApp(ctx, options = {}) {
1541
+ return signalAppProcesses(ctx, options, 'SIGSTOP', 'freeze-app');
1542
+ }
1543
+
1544
+ async function thawApp(ctx, options = {}) {
1545
+ return signalAppProcesses(ctx, options, 'SIGCONT', 'thaw-app');
1546
+ }
1547
+
1548
+ async function signalAppProcesses(ctx, options, signal, action) {
1549
+ const pids = await targetAppPids(ctx, options);
1550
+ if (pids.length === 0) {
1551
+ return {
1552
+ ok: false,
1553
+ packageName: ctx.packageName,
1554
+ action,
1555
+ signal,
1556
+ error: 'app_process_not_found',
1557
+ };
1558
+ }
1559
+
1560
+ const results = [];
1561
+ for (const pid of pids) {
1562
+ try {
1563
+ const result = await adb(ctx, ['shell', 'run-as', ctx.packageName, 'kill', `-${signal}`, pid]);
1564
+ results.push({
1565
+ pid,
1566
+ ok: true,
1567
+ stdout: result.stdout.trim(),
1568
+ stderr: result.stderr.trim(),
1569
+ });
1570
+ } catch (error) {
1571
+ results.push({
1572
+ pid,
1573
+ ok: false,
1574
+ error: firstErrorLine(error),
1575
+ });
1576
+ }
1577
+ }
1578
+
1579
+ const failed = results.filter((item) => !item.ok);
1580
+ return {
1581
+ ok: failed.length === 0,
1582
+ packageName: ctx.packageName,
1583
+ action,
1584
+ signal,
1585
+ pids,
1586
+ results,
1587
+ ...(failed.length > 0 ? { error: 'signal_failed' } : {}),
1588
+ };
1589
+ }
1590
+
1591
+ async function targetAppPids(ctx, options = {}) {
1592
+ if (options.pid && options.pid !== true && options.pid !== 'current') {
1593
+ const explicitPid = String(options.pid).trim();
1594
+ return /^\d+$/.test(explicitPid) ? [explicitPid] : [];
1595
+ }
1596
+ const psPids = await packagePidsFromPs(ctx);
1597
+ if (psPids.length > 0) return psPids;
1598
+ return packagePidsFor(ctx);
1599
+ }
1600
+
1601
+ async function packagePidsFromPs(ctx) {
1602
+ if (!ctx.packageName) return [];
1603
+ try {
1604
+ const result = await adb(ctx, ['shell', 'ps', '-A', '-o', 'PID,NAME']);
1605
+ return parsePackagePidsFromPs(result.stdout, ctx.packageName);
1606
+ } catch (_) {
1607
+ return [];
1608
+ }
1609
+ }
1610
+
1611
+ function parsePackagePidsFromPs(stdout, packageName) {
1612
+ const pids = [];
1613
+ const seen = new Set();
1614
+ for (const line of String(stdout || '').split(/\r?\n/)) {
1615
+ const trimmed = line.trim();
1616
+ if (!trimmed || /^PID\s+NAME$/i.test(trimmed)) continue;
1617
+ const match = /^(\d+)\s+(\S+)$/.exec(trimmed);
1618
+ if (!match) continue;
1619
+ const pid = match[1];
1620
+ const processName = match[2];
1621
+ if (processName !== packageName && !processName.startsWith(`${packageName}:`)) continue;
1622
+ if (seen.has(pid)) continue;
1623
+ seen.add(pid);
1624
+ pids.push(pid);
1625
+ }
1626
+ return pids;
1627
+ }
1628
+
1531
1629
  function parseWebViewDevToolsSockets(procNetUnix, packagePids = []) {
1532
1630
  const packagePidSet = new Set(packagePids.map(String));
1533
1631
  const sockets = [];
@@ -2843,19 +2941,19 @@ async function tapFlutterText(ctx, targetText, options = {}) {
2843
2941
  };
2844
2942
  }
2845
2943
 
2846
- async function inputFlutterText(ctx, targetText, text) {
2847
- const operable = await flutterNodes(ctx);
2848
- const node = findFlutterNode(operable, targetText, 'input');
2849
- if (!node) {
2850
- throw new Error(`flutter input node not found: ${targetText}`);
2851
- }
2852
- const point = flutterNodePoint(node.input?.bounds || node.bounds, operable.viewport);
2853
- const result = await flutterAction(ctx, { action: 'inputText', text, x: point.x, y: point.y });
2854
- return {
2855
- ...result,
2856
- source: 'flutter-operable-tree',
2857
- targetText,
2858
- text,
2944
+ async function inputFlutterText(ctx, targetText, text) {
2945
+ const operable = await flutterNodes(ctx);
2946
+ const node = findFlutterNode(operable, targetText, 'input');
2947
+ if (!node) {
2948
+ throw new Error(`flutter input node not found: ${targetText}`);
2949
+ }
2950
+ const point = flutterNodePoint(node.input?.bounds || node.bounds, operable.viewport);
2951
+ const result = await flutterAction(ctx, { action: 'inputText', text, x: point.x, y: point.y });
2952
+ return {
2953
+ ...result,
2954
+ source: 'flutter-operable-tree',
2955
+ targetText,
2956
+ text,
2859
2957
  node,
2860
2958
  x: point.x,
2861
2959
  y: point.y,
@@ -3529,78 +3627,78 @@ function shouldDismissKeyboardForPoint({ point, viewport, keyboardVisible }) {
3529
3627
  };
3530
3628
  }
3531
3629
 
3532
- async function inputText(ctx, text, options = {}) {
3533
- const bridgePayload = inputTextBridgePayload(text, options);
3534
- let bridgeAttempt = null;
3535
- try {
3536
- const bridgeResult = await bridgePost(ctx, '/v1/action/input-text', bridgePayload);
3537
- if (bridgeResult?.ok) {
3538
- const result = {
3539
- ...bridgeResult,
3540
- transport: 'bridge',
3541
- source: bridgeResult.source || 'native-view',
3542
- request: bridgePayload,
3543
- };
3544
- if (booleanOption(options.hideKeyboard)) {
3545
- result.keyboard = await hideKeyboard(ctx, options);
3546
- }
3547
- return result;
3548
- }
3549
- bridgeAttempt = {
3550
- ok: false,
3551
- command: 'input-text',
3552
- requestPath: '/v1/action/input-text',
3553
- result: bridgeResult,
3554
- error: bridgeResult?.error || 'bridge_input_failed',
3555
- message: bridgeResult?.message || 'The app bridge did not accept native text input.',
3556
- };
3557
- } catch (error) {
3558
- bridgeAttempt = buildBridgeFailureResult(ctx, 'input-text', '/v1/action/input-text', error);
3559
- }
3560
-
3561
- if (!isAdbInputTextSafe(text)) {
3562
- return {
3563
- ok: false,
3564
- error: 'unicode_text_requires_bridge_input',
3565
- message: 'This text contains non-ASCII characters. Android adb shell input text cannot reliably enter Unicode; use an app build with AI App Bridge Android runtime 0.1.9+ and retry input-text/input_text.',
3566
- textLength: text.length,
3567
- bridge: bridgeAttempt,
3568
- suggestion: 'Update the target app bridge dependency to ai-app-bridge-android 0.1.9+ and pass --package-name so the CLI can discover the app bridge port.',
3569
- };
3570
- }
3571
-
3572
- await adb(ctx, ['shell', 'input', 'text', text.replace(/ /g, '%s')]);
3573
- const result = {
3574
- ok: true,
3575
- transport: 'adb',
3576
- source: 'adb-input-text-fallback',
3577
- text,
3578
- bridge: bridgeAttempt,
3579
- };
3580
- if (booleanOption(options.hideKeyboard)) {
3581
- result.keyboard = await hideKeyboard(ctx, options);
3582
- }
3583
- return result;
3584
- }
3585
-
3586
- function inputTextBridgePayload(text, options = {}) {
3587
- const payload = { text };
3588
- const x = Number(options.tapX);
3589
- const y = Number(options.tapY);
3590
- if (Number.isFinite(x) && Number.isFinite(y)) {
3591
- payload.x = x;
3592
- payload.y = y;
3593
- }
3594
- return payload;
3595
- }
3596
-
3597
- function isAdbInputTextSafe(text) {
3598
- return /^[\x20-\x7e]*$/.test(String(text));
3599
- }
3600
-
3601
- async function swipe(ctx, startX, startY, endX, endY, durationMs) {
3602
- await adb(ctx, ['shell', 'input', 'swipe', String(startX), String(startY), String(endX), String(endY), String(durationMs)]);
3603
- return { ok: true, transport: 'adb', startX, startY, endX, endY, durationMs };
3630
+ async function inputText(ctx, text, options = {}) {
3631
+ const bridgePayload = inputTextBridgePayload(text, options);
3632
+ let bridgeAttempt = null;
3633
+ try {
3634
+ const bridgeResult = await bridgePost(ctx, '/v1/action/input-text', bridgePayload);
3635
+ if (bridgeResult?.ok) {
3636
+ const result = {
3637
+ ...bridgeResult,
3638
+ transport: 'bridge',
3639
+ source: bridgeResult.source || 'native-view',
3640
+ request: bridgePayload,
3641
+ };
3642
+ if (booleanOption(options.hideKeyboard)) {
3643
+ result.keyboard = await hideKeyboard(ctx, options);
3644
+ }
3645
+ return result;
3646
+ }
3647
+ bridgeAttempt = {
3648
+ ok: false,
3649
+ command: 'input-text',
3650
+ requestPath: '/v1/action/input-text',
3651
+ result: bridgeResult,
3652
+ error: bridgeResult?.error || 'bridge_input_failed',
3653
+ message: bridgeResult?.message || 'The app bridge did not accept native text input.',
3654
+ };
3655
+ } catch (error) {
3656
+ bridgeAttempt = buildBridgeFailureResult(ctx, 'input-text', '/v1/action/input-text', error);
3657
+ }
3658
+
3659
+ if (!isAdbInputTextSafe(text)) {
3660
+ return {
3661
+ ok: false,
3662
+ error: 'unicode_text_requires_bridge_input',
3663
+ message: 'This text contains non-ASCII characters. Android adb shell input text cannot reliably enter Unicode; use an app build with AI App Bridge Android runtime 0.1.9+ and retry input-text/input_text.',
3664
+ textLength: text.length,
3665
+ bridge: bridgeAttempt,
3666
+ suggestion: 'Update the target app bridge dependency to ai-app-bridge-android 0.1.9+ and pass --package-name so the CLI can discover the app bridge port.',
3667
+ };
3668
+ }
3669
+
3670
+ await adb(ctx, ['shell', 'input', 'text', text.replace(/ /g, '%s')]);
3671
+ const result = {
3672
+ ok: true,
3673
+ transport: 'adb',
3674
+ source: 'adb-input-text-fallback',
3675
+ text,
3676
+ bridge: bridgeAttempt,
3677
+ };
3678
+ if (booleanOption(options.hideKeyboard)) {
3679
+ result.keyboard = await hideKeyboard(ctx, options);
3680
+ }
3681
+ return result;
3682
+ }
3683
+
3684
+ function inputTextBridgePayload(text, options = {}) {
3685
+ const payload = { text };
3686
+ const x = Number(options.tapX);
3687
+ const y = Number(options.tapY);
3688
+ if (Number.isFinite(x) && Number.isFinite(y)) {
3689
+ payload.x = x;
3690
+ payload.y = y;
3691
+ }
3692
+ return payload;
3693
+ }
3694
+
3695
+ function isAdbInputTextSafe(text) {
3696
+ return /^[\x20-\x7e]*$/.test(String(text));
3697
+ }
3698
+
3699
+ async function swipe(ctx, startX, startY, endX, endY, durationMs) {
3700
+ await adb(ctx, ['shell', 'input', 'swipe', String(startX), String(startY), String(endX), String(endY), String(durationMs)]);
3701
+ return { ok: true, transport: 'adb', startX, startY, endX, endY, durationMs };
3604
3702
  }
3605
3703
 
3606
3704
  async function keyevent(ctx, keyCode) {
@@ -3710,6 +3808,59 @@ async function appopsSet(ctx, op, mode) {
3710
3808
  };
3711
3809
  }
3712
3810
 
3811
+ async function clearAppData(ctx) {
3812
+ if (!ctx.explicitPackageName) {
3813
+ throw new Error('packageName is required for clear-app-data');
3814
+ }
3815
+ let bridgeError = null;
3816
+ try {
3817
+ const bridgeResult = await bridgePost(ctx, '/v1/app/clear-data', {});
3818
+ return {
3819
+ ...bridgeResult,
3820
+ packageName: ctx.packageName,
3821
+ method: 'bridge-runtime',
3822
+ };
3823
+ } catch (error) {
3824
+ bridgeError = firstErrorLine(error);
3825
+ }
3826
+
3827
+ let result;
3828
+ try {
3829
+ result = await adb(ctx, clearAppDataAdbArgs(ctx.packageName));
3830
+ } catch (error) {
3831
+ return {
3832
+ ok: false,
3833
+ packageName: ctx.packageName,
3834
+ action: 'clear-app-data',
3835
+ error: 'clear_app_data_failed',
3836
+ bridgeError,
3837
+ message: error.message || String(error),
3838
+ };
3839
+ }
3840
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
3841
+ if (!/^Success\b/im.test(output)) {
3842
+ return {
3843
+ ok: false,
3844
+ packageName: ctx.packageName,
3845
+ error: 'clear_app_data_failed',
3846
+ bridgeError,
3847
+ output,
3848
+ };
3849
+ }
3850
+ return {
3851
+ ok: true,
3852
+ packageName: ctx.packageName,
3853
+ action: 'clear-app-data',
3854
+ method: 'pm-clear',
3855
+ bridgeError,
3856
+ output,
3857
+ };
3858
+ }
3859
+
3860
+ function clearAppDataAdbArgs(packageName) {
3861
+ return ['shell', 'pm', 'clear', packageName];
3862
+ }
3863
+
3713
3864
  async function resolveLogcatPid(ctx, options) {
3714
3865
  if (options.pid && options.pid !== true && options.pid !== 'current') {
3715
3866
  return String(options.pid);
@@ -3725,17 +3876,17 @@ async function resolveLogcatPid(ctx, options) {
3725
3876
  return '';
3726
3877
  }
3727
3878
 
3728
- function filterLogcat(text, options) {
3729
- const tags = splitCsv(options.tag || options.tags);
3730
- const grep = options.grep ? String(options.grep) : '';
3731
- const grepCaseSensitive = Boolean(options.grepCaseSensitive);
3732
- const minLevel = priorityValue(options.level || options.minLevel || '');
3733
- const pid = options.pid ? String(options.pid) : '';
3734
- const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
3735
- if (requiresAppPid && !pid) {
3736
- return '';
3737
- }
3738
- const lines = String(text || '').split(/\r?\n/);
3879
+ function filterLogcat(text, options) {
3880
+ const tags = splitCsv(options.tag || options.tags);
3881
+ const grep = options.grep ? String(options.grep) : '';
3882
+ const grepCaseSensitive = Boolean(options.grepCaseSensitive);
3883
+ const minLevel = priorityValue(options.level || options.minLevel || '');
3884
+ const pid = options.pid ? String(options.pid) : '';
3885
+ const requiresAppPid = options.appPid || options.packagePid || options.pid === 'current';
3886
+ if (requiresAppPid && !pid) {
3887
+ return '';
3888
+ }
3889
+ const lines = String(text || '').split(/\r?\n/);
3739
3890
  const filtered = [];
3740
3891
  let previousIncluded = false;
3741
3892
  for (const line of lines) {
@@ -3818,134 +3969,134 @@ function adbFollow(ctx, args, durationMs) {
3818
3969
  });
3819
3970
  }
3820
3971
 
3821
- async function launchApp(ctx, options = {}) {
3822
- if (options.component || options.activity) {
3823
- return launchActivity(ctx, options);
3824
- }
3825
-
3826
- const candidates = await launcherActivityCandidates(ctx);
3827
- if (candidates.length === 0) {
3828
- return {
3829
- ok: false,
3830
- error: 'launcher_not_found',
3831
- packageName: ctx.packageName,
3832
- launcherCandidates: candidates,
3833
- };
3834
- }
3835
- if (candidates.length > 1) {
3836
- return {
3837
- ok: false,
3838
- error: 'launcher_ambiguous',
3839
- packageName: ctx.packageName,
3840
- launcherCandidates: candidates,
3841
- suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
3842
- };
3843
- }
3844
-
3845
- return startActivity(ctx, candidates[0], options, {
3846
- packageName: ctx.packageName,
3847
- launcherCandidates: candidates,
3848
- });
3849
- }
3850
-
3851
- async function launchActivity(ctx, options = {}) {
3852
- const component = normalizeActivityComponent(
3853
- ctx.packageName,
3854
- options.component || requiredString(options.activity, 'activity'),
3855
- );
3856
- return startActivity(ctx, component, options, { packageName: ctx.packageName });
3857
- }
3858
-
3859
- async function launchNativeTest(ctx) {
3860
- const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
3861
- return startActivity(ctx, component, {}, { packageName: ctx.packageName });
3862
- }
3863
-
3864
- async function launchFlutter(ctx, initialRoute) {
3865
- const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
3866
- const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
3867
- const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
3868
- return { ...result, initialRoute };
3869
- }
3870
-
3871
- async function launcherActivityCandidates(ctx) {
3872
- const result = await adb(ctx, [
3873
- 'shell',
3874
- 'cmd',
3875
- 'package',
3876
- 'query-activities',
3877
- '--brief',
3878
- '-a',
3879
- 'android.intent.action.MAIN',
3880
- '-c',
3881
- 'android.intent.category.LAUNCHER',
3882
- ctx.packageName,
3883
- ]);
3884
- return parseLauncherActivityCandidates(result.stdout);
3885
- }
3886
-
3887
- function parseLauncherActivityCandidates(stdout) {
3888
- const candidates = [];
3889
- for (const line of String(stdout || '').split(/\r?\n/)) {
3890
- const candidate = line.trim();
3891
- if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
3892
- candidates.push(candidate);
3893
- }
3894
- }
3895
- return candidates;
3896
- }
3897
-
3898
- function normalizeActivityComponent(packageName, activityOrComponent) {
3899
- const value = requiredString(activityOrComponent, 'activity');
3900
- if (value.includes('/')) return value;
3901
- return `${packageName}/${value}`;
3902
- }
3903
-
3904
- async function startActivity(ctx, component, options = {}, extraResult = {}) {
3905
- const args = buildAmStartArgs(component, options);
3906
- const result = await adb(ctx, args);
3907
- return {
3908
- ok: true,
3909
- transport: 'adb',
3910
- component,
3911
- ...extraResult,
3912
- stdout: result.stdout.trim(),
3913
- stderr: result.stderr.trim(),
3914
- };
3915
- }
3916
-
3917
- function buildAmStartArgs(component, options = {}) {
3918
- const args = ['shell', 'am', 'start'];
3919
- if (options.action) args.push('-a', options.action);
3920
- for (const category of optionList(options.category)) {
3921
- args.push('-c', category);
3922
- }
3923
- if (options.data) args.push('-d', options.data);
3924
- for (const extra of parseStartExtras(options.extra)) {
3925
- args.push('-e', extra.key, extra.value);
3926
- }
3927
- args.push('-n', component);
3928
- return args;
3929
- }
3930
-
3931
- function parseStartExtras(rawExtras) {
3932
- return optionList(rawExtras).map((rawExtra) => {
3933
- const value = String(rawExtra);
3934
- const separator = value.indexOf('=');
3935
- if (separator <= 0) {
3936
- throw new Error('extra must use key=value');
3937
- }
3938
- return {
3939
- key: value.slice(0, separator),
3940
- value: value.slice(separator + 1),
3941
- };
3942
- });
3943
- }
3944
-
3945
- function optionList(value) {
3946
- if (value === undefined || value === null || value === false || value === '') return [];
3947
- return Array.isArray(value) ? value : [value];
3948
- }
3972
+ async function launchApp(ctx, options = {}) {
3973
+ if (options.component || options.activity) {
3974
+ return launchActivity(ctx, options);
3975
+ }
3976
+
3977
+ const candidates = await launcherActivityCandidates(ctx);
3978
+ if (candidates.length === 0) {
3979
+ return {
3980
+ ok: false,
3981
+ error: 'launcher_not_found',
3982
+ packageName: ctx.packageName,
3983
+ launcherCandidates: candidates,
3984
+ };
3985
+ }
3986
+ if (candidates.length > 1) {
3987
+ return {
3988
+ ok: false,
3989
+ error: 'launcher_ambiguous',
3990
+ packageName: ctx.packageName,
3991
+ launcherCandidates: candidates,
3992
+ suggestion: 'Pass --component or --activity to choose the intended launcher Activity.',
3993
+ };
3994
+ }
3995
+
3996
+ return startActivity(ctx, candidates[0], options, {
3997
+ packageName: ctx.packageName,
3998
+ launcherCandidates: candidates,
3999
+ });
4000
+ }
4001
+
4002
+ async function launchActivity(ctx, options = {}) {
4003
+ const component = normalizeActivityComponent(
4004
+ ctx.packageName,
4005
+ options.component || requiredString(options.activity, 'activity'),
4006
+ );
4007
+ return startActivity(ctx, component, options, { packageName: ctx.packageName });
4008
+ }
4009
+
4010
+ async function launchNativeTest(ctx) {
4011
+ const component = normalizeActivityComponent(ctx.packageName, ctx.nativeActivity);
4012
+ return startActivity(ctx, component, {}, { packageName: ctx.packageName });
4013
+ }
4014
+
4015
+ async function launchFlutter(ctx, initialRoute) {
4016
+ const component = normalizeActivityComponent(ctx.packageName, ctx.flutterActivity);
4017
+ const options = initialRoute ? { extra: [`ai_app_initial_route=${initialRoute}`] } : {};
4018
+ const result = await startActivity(ctx, component, options, { packageName: ctx.packageName });
4019
+ return { ...result, initialRoute };
4020
+ }
4021
+
4022
+ async function launcherActivityCandidates(ctx) {
4023
+ const result = await adb(ctx, [
4024
+ 'shell',
4025
+ 'cmd',
4026
+ 'package',
4027
+ 'query-activities',
4028
+ '--brief',
4029
+ '-a',
4030
+ 'android.intent.action.MAIN',
4031
+ '-c',
4032
+ 'android.intent.category.LAUNCHER',
4033
+ ctx.packageName,
4034
+ ]);
4035
+ return parseLauncherActivityCandidates(result.stdout);
4036
+ }
4037
+
4038
+ function parseLauncherActivityCandidates(stdout) {
4039
+ const candidates = [];
4040
+ for (const line of String(stdout || '').split(/\r?\n/)) {
4041
+ const candidate = line.trim();
4042
+ if (/^[A-Za-z0-9_.$]+\/[A-Za-z0-9_.$]+$/.test(candidate) && !candidates.includes(candidate)) {
4043
+ candidates.push(candidate);
4044
+ }
4045
+ }
4046
+ return candidates;
4047
+ }
4048
+
4049
+ function normalizeActivityComponent(packageName, activityOrComponent) {
4050
+ const value = requiredString(activityOrComponent, 'activity');
4051
+ if (value.includes('/')) return value;
4052
+ return `${packageName}/${value}`;
4053
+ }
4054
+
4055
+ async function startActivity(ctx, component, options = {}, extraResult = {}) {
4056
+ const args = buildAmStartArgs(component, options);
4057
+ const result = await adb(ctx, args);
4058
+ return {
4059
+ ok: true,
4060
+ transport: 'adb',
4061
+ component,
4062
+ ...extraResult,
4063
+ stdout: result.stdout.trim(),
4064
+ stderr: result.stderr.trim(),
4065
+ };
4066
+ }
4067
+
4068
+ function buildAmStartArgs(component, options = {}) {
4069
+ const args = ['shell', 'am', 'start'];
4070
+ if (options.action) args.push('-a', options.action);
4071
+ for (const category of optionList(options.category)) {
4072
+ args.push('-c', category);
4073
+ }
4074
+ if (options.data) args.push('-d', options.data);
4075
+ for (const extra of parseStartExtras(options.extra)) {
4076
+ args.push('-e', extra.key, extra.value);
4077
+ }
4078
+ args.push('-n', component);
4079
+ return args;
4080
+ }
4081
+
4082
+ function parseStartExtras(rawExtras) {
4083
+ return optionList(rawExtras).map((rawExtra) => {
4084
+ const value = String(rawExtra);
4085
+ const separator = value.indexOf('=');
4086
+ if (separator <= 0) {
4087
+ throw new Error('extra must use key=value');
4088
+ }
4089
+ return {
4090
+ key: value.slice(0, separator),
4091
+ value: value.slice(separator + 1),
4092
+ };
4093
+ });
4094
+ }
4095
+
4096
+ function optionList(value) {
4097
+ if (value === undefined || value === null || value === false || value === '') return [];
4098
+ return Array.isArray(value) ? value : [value];
4099
+ }
3949
4100
 
3950
4101
  async function smoke(ctx, options) {
3951
4102
  const summary = {
@@ -4304,6 +4455,7 @@ function requiredNumber(value, name) {
4304
4455
 
4305
4456
  module.exports = {
4306
4457
  buildBridgeFailureResult,
4458
+ clearAppDataAdbArgs,
4307
4459
  defaultInstallerButtonTexts,
4308
4460
  artifactTimestamp,
4309
4461
  compactBridgeTree,
@@ -4311,41 +4463,42 @@ module.exports = {
4311
4463
  compactUiaTree,
4312
4464
  defaultArtifactDirectory,
4313
4465
  defaultArtifactPath,
4314
- findFlutterNode,
4315
- findTappableNodeByText,
4316
- filterLogcat,
4317
- firstErrorLine,
4466
+ findFlutterNode,
4467
+ findTappableNodeByText,
4468
+ filterLogcat,
4469
+ firstErrorLine,
4318
4470
  flutterNodePoint,
4319
4471
  flutterPhysicalViewport,
4320
4472
  helpText,
4321
- installerButtonTextsForSurface,
4322
- isAdbInputTextSafe,
4323
- isLikelyInstallerSurface,
4473
+ installerButtonTextsForSurface,
4474
+ isAdbInputTextSafe,
4475
+ isLikelyInstallerSurface,
4324
4476
  nodeTapState,
4325
- normalizeBridgeError,
4326
- normalizeActivityComponent,
4327
- parseWebViewDevToolsSockets,
4328
- parseArgs,
4329
- parseKeyboardState,
4330
- parseLauncherActivityCandidates,
4331
- parseStartExtras,
4332
- parseUiaBounds,
4477
+ normalizeBridgeError,
4478
+ normalizeActivityComponent,
4479
+ parseWebViewDevToolsSockets,
4480
+ parseArgs,
4481
+ parseKeyboardState,
4482
+ parsePackagePidsFromPs,
4483
+ parseLauncherActivityCandidates,
4484
+ parseStartExtras,
4485
+ parseUiaBounds,
4333
4486
  parseUiaViewport,
4334
4487
  parseComponentFromWindowLine,
4335
4488
  parseForegroundWindow,
4336
4489
  chooseWebViewDevToolsSocket,
4337
4490
  chooseWebViewPage,
4338
- shapeNetworkCapture,
4339
- compactNetworkRecord,
4340
- pruneGeneratedArtifacts,
4341
- shouldSkipInstallerTapForInstalledPackage,
4342
- shouldDismissKeyboardForPoint,
4343
- shouldUseDefaultPortFallback,
4344
- screenshotOutputPath,
4345
- statusSearchText,
4346
- uiautomatorLockPath,
4347
- verifyBridgeTargetPackage,
4348
- waitTextConditionsMet,
4349
- withFileLock,
4350
- };
4491
+ shapeNetworkCapture,
4492
+ compactNetworkRecord,
4493
+ pruneGeneratedArtifacts,
4494
+ shouldSkipInstallerTapForInstalledPackage,
4495
+ shouldDismissKeyboardForPoint,
4496
+ shouldUseDefaultPortFallback,
4497
+ screenshotOutputPath,
4498
+ statusSearchText,
4499
+ uiautomatorLockPath,
4500
+ verifyBridgeTargetPackage,
4501
+ waitTextConditionsMet,
4502
+ withFileLock,
4503
+ };
4351
4504