@mobileaidev/ai-app-bridge 0.2.5 → 0.2.6

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
@@ -3,9 +3,10 @@
3
3
  ```bash
4
4
  npm install -g @mobileaidev/ai-app-bridge
5
5
 
6
- ai-app-bridge status --package-name io.github.mobileaidev.aiappbridge.sample
6
+ ai-app-bridge status --package-name io.github.mobileaidev.aiappbridge.sample
7
7
  ai-app-bridge tree --package-name io.github.mobileaidev.aiappbridge.sample
8
8
  ai-app-bridge install-apk --package-name io.github.mobileaidev.aiappbridge.sample --apk-path app-debug.apk
9
+ ai-app-bridge clear-app-data --package-name io.github.mobileaidev.aiappbridge.sample
9
10
  ai-app-bridge launch-app --package-name io.github.mobileaidev.aiappbridge.sample
10
11
  ai-app-bridge launch-activity --package-name io.github.mobileaidev.aiappbridge.sample --activity .MainActivity --extra route=/home
11
12
  ai-app-bridge screenshot --package-name io.github.mobileaidev.aiappbridge.sample
@@ -21,12 +22,17 @@ into the model context:
21
22
  - `capabilities` lists the bridge domains and command names.
22
23
  - `run` executes a selected command with command-specific arguments.
23
24
 
24
- This keeps install, launch, UI, Flutter, WebView, logcat, network, and
25
- permission capabilities discoverable without exposing dozens of full schemas at
26
- session start. Set `AI_APP_BRIDGE_MCP_SURFACE=full` before launching
25
+ This keeps install, data reset, launch, UI, Flutter, WebView, logcat, network,
26
+ and permission capabilities discoverable without exposing dozens of full schemas
27
+ at session start. Set `AI_APP_BRIDGE_MCP_SURFACE=full` before launching
27
28
  `ai-app-bridge-mcp` only when a client needs the legacy one-tool-per-command
28
29
  surface.
29
30
 
31
+ The MCP server accepts both standard `Content-Length` framed JSON-RPC messages
32
+ and single-line JSON messages. Responses use the format of the first request on
33
+ that connection, so standard MCP clients keep framed responses while local
34
+ Node REPL scripts can send and read one JSON object per line.
35
+
30
36
  For multi-step app automation, call `run` with `command: "batch"`. Batch steps
31
37
  run serially in one MCP call, so a failed step can stop and mark the remaining
32
38
  steps as skipped without mixing results from different commands:
@@ -70,6 +70,7 @@ Device/action commands:
70
70
 
71
71
  App/permission commands:
72
72
  install-apk Install an APK and assist device-side installer screens.
73
+ clear-app-data Clear target app local data through the bridge runtime.
73
74
  launch-app Launch the target package LAUNCHER Activity.
74
75
  launch-activity Launch an explicit Android Activity component.
75
76
  launch-native-test Launch the debug native Android bridge test Activity.
@@ -296,10 +297,12 @@ async function runCommand(command, options, ctx) {
296
297
  return keyboardState(ctx);
297
298
  case 'hide-keyboard':
298
299
  return hideKeyboard(ctx, options);
299
- case 'install-apk':
300
- return installApk(ctx, options);
301
- case 'webview-pages':
302
- return webviewPages(ctx, options);
300
+ case 'install-apk':
301
+ return installApk(ctx, options);
302
+ case 'clear-app-data':
303
+ return clearAppData(ctx);
304
+ case 'webview-pages':
305
+ return webviewPages(ctx, options);
303
306
  case 'webview-network':
304
307
  return webviewCdpCapture(ctx, { ...options, captureNetwork: true, captureConsole: true });
305
308
  case 'webview-console':
@@ -3700,17 +3703,70 @@ async function safePermissionState(ctx, permission) {
3700
3703
  }
3701
3704
  }
3702
3705
 
3703
- async function appopsSet(ctx, op, mode) {
3704
- await adb(ctx, ['shell', 'appops', 'set', ctx.packageName, op, mode]);
3705
- return {
3706
+ async function appopsSet(ctx, op, mode) {
3707
+ await adb(ctx, ['shell', 'appops', 'set', ctx.packageName, op, mode]);
3708
+ return {
3706
3709
  ok: true,
3707
3710
  packageName: ctx.packageName,
3708
3711
  op,
3709
3712
  mode,
3710
- };
3711
- }
3712
-
3713
- async function resolveLogcatPid(ctx, options) {
3713
+ };
3714
+ }
3715
+
3716
+ async function clearAppData(ctx) {
3717
+ if (!ctx.explicitPackageName) {
3718
+ throw new Error('packageName is required for clear-app-data');
3719
+ }
3720
+ let bridgeError = null;
3721
+ try {
3722
+ const bridgeResult = await bridgePost(ctx, '/v1/app/clear-data', {});
3723
+ return {
3724
+ ...bridgeResult,
3725
+ packageName: ctx.packageName,
3726
+ method: 'bridge-runtime',
3727
+ };
3728
+ } catch (error) {
3729
+ bridgeError = firstErrorLine(error);
3730
+ }
3731
+
3732
+ let result;
3733
+ try {
3734
+ result = await adb(ctx, clearAppDataAdbArgs(ctx.packageName));
3735
+ } catch (error) {
3736
+ return {
3737
+ ok: false,
3738
+ packageName: ctx.packageName,
3739
+ action: 'clear-app-data',
3740
+ error: 'clear_app_data_failed',
3741
+ bridgeError,
3742
+ message: error.message || String(error),
3743
+ };
3744
+ }
3745
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
3746
+ if (!/^Success\b/im.test(output)) {
3747
+ return {
3748
+ ok: false,
3749
+ packageName: ctx.packageName,
3750
+ error: 'clear_app_data_failed',
3751
+ bridgeError,
3752
+ output,
3753
+ };
3754
+ }
3755
+ return {
3756
+ ok: true,
3757
+ packageName: ctx.packageName,
3758
+ action: 'clear-app-data',
3759
+ method: 'pm-clear',
3760
+ bridgeError,
3761
+ output,
3762
+ };
3763
+ }
3764
+
3765
+ function clearAppDataAdbArgs(packageName) {
3766
+ return ['shell', 'pm', 'clear', packageName];
3767
+ }
3768
+
3769
+ async function resolveLogcatPid(ctx, options) {
3714
3770
  if (options.pid && options.pid !== true && options.pid !== 'current') {
3715
3771
  return String(options.pid);
3716
3772
  }
@@ -4303,8 +4359,9 @@ function requiredNumber(value, name) {
4303
4359
  }
4304
4360
 
4305
4361
  module.exports = {
4306
- buildBridgeFailureResult,
4307
- defaultInstallerButtonTexts,
4362
+ buildBridgeFailureResult,
4363
+ clearAppDataAdbArgs,
4364
+ defaultInstallerButtonTexts,
4308
4365
  artifactTimestamp,
4309
4366
  compactBridgeTree,
4310
4367
  compactStatus,
package/bin/mcp-server.js CHANGED
@@ -11,12 +11,13 @@ const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
11
11
  const defaultProtocolVersion = supportedProtocolVersions[0];
12
12
  const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
13
13
  const serverInstructions = [
14
- 'AI App Bridge observes and controls Android apps for agent workflows. Prefer these tools over raw adb when inspecting UI, text, WebView, logs, network, app install, launch, and permissions.',
14
+ 'AI App Bridge observes and controls Android apps for agent workflows. Prefer these tools over raw adb when inspecting UI, text, WebView, logs, network, app install, data reset, launch, and permissions.',
15
15
  'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
16
16
  'Always pass packageName for app-specific commands, or pass an explicit port. Do not rely on a sample/default package in MCP sessions.',
17
17
  ].join(' ');
18
18
 
19
19
  let buffer = Buffer.alloc(0);
20
+ let responseFormat = null;
20
21
 
21
22
  function startServer() {
22
23
  process.stdin.on('data', (chunk) => {
@@ -27,34 +28,77 @@ function startServer() {
27
28
  process.stdin.on('error', () => {});
28
29
  }
29
30
 
30
- function drainMessages() {
31
- while (true) {
32
- const delimiter = findHeaderDelimiter(buffer);
33
- const headerEnd = delimiter.index;
34
- if (headerEnd < 0) {
35
- return;
36
- }
37
- const header = buffer.subarray(0, headerEnd).toString('utf8');
38
- const match = /^Content-Length:\s*(\d+)$/im.exec(header);
39
- if (!match) {
40
- buffer = buffer.subarray(headerEnd + delimiter.length);
41
- continue;
42
- }
43
- const contentLength = Number(match[1]);
44
- const messageStart = headerEnd + delimiter.length;
45
- const messageEnd = messageStart + contentLength;
46
- if (buffer.length < messageEnd) {
47
- return;
48
- }
49
- const body = buffer.subarray(messageStart, messageEnd).toString('utf8');
50
- buffer = buffer.subarray(messageEnd);
51
- handleMessage(body).catch((error) => {
52
- writeLog(`unhandled message error: ${error.stack || error}`);
53
- });
54
- }
55
- }
56
-
57
- function findHeaderDelimiter(source) {
31
+ function drainMessages() {
32
+ while (true) {
33
+ const parsed = readNextMessage(buffer);
34
+ if (!parsed) {
35
+ return;
36
+ }
37
+ buffer = parsed.remaining;
38
+ setResponseFormat(parsed.format);
39
+ handleMessage(parsed.body).catch((error) => {
40
+ writeLog(`unhandled message error: ${error.stack || error}`);
41
+ });
42
+ }
43
+ }
44
+
45
+ function readNextMessage(source) {
46
+ const text = source.toString('utf8');
47
+ if (/^Content-Length:/i.test(text)) {
48
+ return readContentLengthMessage(source);
49
+ }
50
+ return readLineJsonMessage(source);
51
+ }
52
+
53
+ function readContentLengthMessage(source) {
54
+ const delimiter = findHeaderDelimiter(source);
55
+ const headerEnd = delimiter.index;
56
+ if (headerEnd < 0) {
57
+ return null;
58
+ }
59
+ const header = source.subarray(0, headerEnd).toString('utf8');
60
+ const match = /^Content-Length:\s*(\d+)$/im.exec(header);
61
+ if (!match) {
62
+ return {
63
+ body: source.subarray(headerEnd + delimiter.length).toString('utf8'),
64
+ format: 'frame',
65
+ remaining: Buffer.alloc(0),
66
+ };
67
+ }
68
+ const contentLength = Number(match[1]);
69
+ const messageStart = headerEnd + delimiter.length;
70
+ const messageEnd = messageStart + contentLength;
71
+ if (source.length < messageEnd) {
72
+ return null;
73
+ }
74
+ return {
75
+ body: source.subarray(messageStart, messageEnd).toString('utf8'),
76
+ format: 'frame',
77
+ remaining: source.subarray(messageEnd),
78
+ };
79
+ }
80
+
81
+ function readLineJsonMessage(source) {
82
+ const lfIndex = source.indexOf('\n');
83
+ if (lfIndex < 0) {
84
+ return null;
85
+ }
86
+ const lineEnd = lfIndex > 0 && source[lfIndex - 1] === 13 ? lfIndex - 1 : lfIndex;
87
+ const body = source.subarray(0, lineEnd).toString('utf8');
88
+ return {
89
+ body,
90
+ format: 'line',
91
+ remaining: source.subarray(lfIndex + 1),
92
+ };
93
+ }
94
+
95
+ function setResponseFormat(format) {
96
+ if (!responseFormat) {
97
+ responseFormat = format;
98
+ }
99
+ }
100
+
101
+ function findHeaderDelimiter(source) {
58
102
  const crlfIndex = source.indexOf('\r\n\r\n');
59
103
  const lfIndex = source.indexOf('\n\n');
60
104
  if (crlfIndex < 0) {
@@ -264,14 +308,15 @@ function fullToolDefinitions() {
264
308
  bridgeTool('events', 'Read generic in-app event records.'),
265
309
  bridgeTool('uia_tree', 'Read UIAutomator XML for the current device window.'),
266
310
  bridgeTool('screenshot', 'Capture an ADB screenshot.'),
267
- bridgeTool('install_apk', 'Install an APK through ADB while assisting device-side package-installer confirmation screens with UIAutomator.', {
268
- apkPath: { type: 'string', description: 'Absolute or workspace-relative APK path.' },
269
- allowDowngrade: { type: 'boolean', description: 'Pass -d to adb install.' },
270
- streaming: { type: 'boolean', description: 'Use streaming install instead of the default --no-streaming mode.' },
271
- installTimeoutMs: { type: 'number', description: 'Maximum time for adb install. Defaults to 180000 ms.' },
272
- installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
273
- intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
274
- }, ['apkPath']),
311
+ bridgeTool('install_apk', 'Install an APK through ADB while assisting device-side package-installer confirmation screens with UIAutomator.', {
312
+ apkPath: { type: 'string', description: 'Absolute or workspace-relative APK path.' },
313
+ allowDowngrade: { type: 'boolean', description: 'Pass -d to adb install.' },
314
+ streaming: { type: 'boolean', description: 'Use streaming install instead of the default --no-streaming mode.' },
315
+ installTimeoutMs: { type: 'number', description: 'Maximum time for adb install. Defaults to 180000 ms.' },
316
+ installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
317
+ intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
318
+ }, ['apkPath']),
319
+ bridgeTool('clear_app_data', 'Clear target app local data through the bridge runtime. Requires packageName so it cannot target the sample package by default.', {}, ['packageName']),
275
320
  bridgeTool('launch_app', 'Launch the target package LAUNCHER Activity. If multiple launcher Activities exist, returns launcher_ambiguous with candidates unless activity or component is explicit.', launchProperties()),
276
321
  bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
277
322
  bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
@@ -405,6 +450,7 @@ const commandDefinitions = [
405
450
  { command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
406
451
  { command: 'logcat', domain: 'diagnostics', summary: 'Read Android logcat with optional app pid, tag, level, and grep filters.', options: ['serial', 'packageName', 'pid', 'appPid', 'tag', 'level', 'grep', 'lines', 'since', 'follow', 'durationSec', 'clear'] },
407
452
  { command: 'install-apk', domain: 'app', summary: 'Install an APK and assist device-side installer confirmation screens.', options: ['serial', 'packageName', 'apkPath', 'allowDowngrade', 'streaming', 'installTimeoutMs', 'installerTimeoutMs', 'intervalMs'] },
453
+ { command: 'clear-app-data', domain: 'app', summary: 'Clear target app local data through the bridge runtime.', targetApp: true, options: ['serial', 'packageName'] },
408
454
  { command: 'launch-app', domain: 'app', summary: 'Launch the target package LAUNCHER Activity and report launcher candidates.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra'] },
409
455
  { command: 'launch-activity', domain: 'app', summary: 'Launch an explicit Android Activity component with optional string extras.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra'] },
410
456
  { command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
@@ -481,6 +527,7 @@ async function callTool(name, args) {
481
527
  input_flutter_text: 'input-flutter-text',
482
528
  uia_tree: 'uia-tree',
483
529
  install_apk: 'install-apk',
530
+ clear_app_data: 'clear-app-data',
484
531
  launch_app: 'launch-app',
485
532
  launch_activity: 'launch-activity',
486
533
  launch_native_test: 'launch-native-test',
@@ -526,7 +573,7 @@ function capabilityPayload(args = {}) {
526
573
  return {
527
574
  ok: true,
528
575
  surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
529
- usage: 'Use run with one of these command names. Prefer packageName for app-specific commands; install-apk, launch-app, UI, WebView, logcat, network, and permission workflows are supported.',
576
+ usage: 'Use run with one of these command names. Prefer packageName for app-specific commands; install-apk, clear-app-data, launch-app, UI, WebView, logcat, network, and permission workflows are supported.',
530
577
  domains,
531
578
  };
532
579
  }
@@ -564,6 +611,9 @@ function normalizeCommandName(value) {
564
611
  }
565
612
 
566
613
  function runBridgeChecked(command, args = {}) {
614
+ if (command === 'clear-app-data' && !args.packageName) {
615
+ return toolText('clear-app-data: packageName is required in MCP mode so the command cannot clear a default package.', true);
616
+ }
567
617
  const definition = commandByName.get(command);
568
618
  if (definition?.targetApp && !args.packageName && !args.port) {
569
619
  return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
@@ -1014,11 +1064,15 @@ function sendError(id, code, message) {
1014
1064
  });
1015
1065
  }
1016
1066
 
1017
- function send(message) {
1018
- const body = Buffer.from(JSON.stringify(message), 'utf8');
1019
- process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
1020
- process.stdout.write(body);
1021
- }
1067
+ function send(message) {
1068
+ const body = Buffer.from(JSON.stringify(message), 'utf8');
1069
+ if (responseFormat === 'line') {
1070
+ process.stdout.write(`${body.toString('utf8')}\n`);
1071
+ return;
1072
+ }
1073
+ process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
1074
+ process.stdout.write(body);
1075
+ }
1022
1076
 
1023
1077
  function writeLog(text) {
1024
1078
  process.stderr.write(`${text}\n`);
@@ -1030,6 +1084,7 @@ if (require.main === module) {
1030
1084
 
1031
1085
  module.exports = {
1032
1086
  buildBridgeCliArgs,
1087
+ readNextMessage,
1033
1088
  runBatch,
1034
1089
  startServer,
1035
1090
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobileaidev/ai-app-bridge",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Desktop CLI and MCP server for AI App Bridge.",
5
5
  "repository": {
6
6
  "type": "git",