@mobileaidev/ai-app-bridge 0.2.8 → 0.2.10
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 +9 -7
- package/bin/ai-app-bridge.js +0 -0
- package/bin/mcp-server.js +86 -54
- package/bin/web-provider.js +618 -0
- package/package.json +30 -29
- package/skills/ai-app-bridge-use/SKILL.md +260 -183
- package/skills/ai-app-bridge-use/agents/openai.yaml +4 -4
package/README.md
CHANGED
|
@@ -66,13 +66,15 @@ steps as skipped without mixing results from different commands:
|
|
|
66
66
|
}
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
For dynamic screens, MCP agents
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
For dynamic or transient screens, MCP agents can use `freeze-app`/`thaw-app` as
|
|
70
|
+
an optional stabilization control: thaw before reads, actions, waits, or
|
|
71
|
+
captures; freeze after evidence capture only when a changing UI would make
|
|
72
|
+
reasoning unreliable; and thaw before the next app operation or before
|
|
73
|
+
finishing so the app is not left frozen. Static screens and ordinary form
|
|
74
|
+
flows usually do not need freezing.
|
|
75
|
+
For visible state changes such as panels, dialogs, page transitions, tabs, or
|
|
76
|
+
button-triggered content, verify with both `screenshot` and `tree`/`uia-tree`;
|
|
77
|
+
do not conclude success from UI tree alone.
|
|
76
78
|
|
|
77
79
|
WebView network and console capture use Android WebView DevTools/CDP when the
|
|
78
80
|
target app is debuggable and WebView debugging is enabled.
|
package/bin/ai-app-bridge.js
CHANGED
|
File without changes
|
package/bin/mcp-server.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawn } = require('child_process');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
const packageInfo = require('../package.json');
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const packageInfo = require('../package.json');
|
|
7
|
+
const { WebBridgeProvider } = require('./web-provider');
|
|
8
|
+
const bridgeDir = __dirname;
|
|
9
|
+
const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
|
|
10
|
+
const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
|
|
10
11
|
const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
|
|
11
12
|
const defaultProtocolVersion = supportedProtocolVersions[0];
|
|
12
13
|
const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
|
|
@@ -14,8 +15,9 @@ const serverInstructions = [
|
|
|
14
15
|
'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
16
|
'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
|
|
16
17
|
'Always pass packageName for app-specific commands, or pass an explicit port. Do not rely on a sample/default package in MCP sessions.',
|
|
17
|
-
'
|
|
18
|
-
].join(' ');
|
|
18
|
+
'Use freeze-app/thaw-app only as an optional stabilization control for dynamic or transient screens: thaw before reads/actions/captures, freeze after evidence capture only when it helps reasoning, and thaw before the next operation or before finishing so the app is not left frozen.',
|
|
19
|
+
].join(' ');
|
|
20
|
+
const webProvider = new WebBridgeProvider();
|
|
19
21
|
|
|
20
22
|
let buffer = Buffer.alloc(0);
|
|
21
23
|
let responseFormat = null;
|
|
@@ -257,10 +259,10 @@ function fullToolDefinitions() {
|
|
|
257
259
|
deltaY: { type: 'number', description: 'Window scroll delta Y when no selector/text is supplied.' },
|
|
258
260
|
}),
|
|
259
261
|
bridgeTool('logs', 'Read generic in-app log records.'),
|
|
260
|
-
bridgeTool('freeze_app', '
|
|
262
|
+
bridgeTool('freeze_app', 'Optionally stop target app processes with SIGSTOP when a dynamic or transient screen needs stable evidence for review.', {
|
|
261
263
|
pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
|
|
262
264
|
}, ['packageName']),
|
|
263
|
-
bridgeTool('thaw_app', 'Resume
|
|
265
|
+
bridgeTool('thaw_app', 'Resume target app processes with SIGCONT before reads, waits, captures, or actions, and before finishing any task that used freeze-app.', {
|
|
264
266
|
pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
|
|
265
267
|
}, ['packageName']),
|
|
266
268
|
bridgeTool('logcat', 'Read Android logcat through ADB with optional pid/tag/level/grep filters.', {
|
|
@@ -458,8 +460,8 @@ const commandDefinitions = [
|
|
|
458
460
|
{ 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'] },
|
|
459
461
|
{ 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'] },
|
|
460
462
|
{ command: 'clear-app-data', domain: 'app', summary: 'Clear target app local data through the bridge runtime.', targetApp: true, options: ['serial', 'packageName'] },
|
|
461
|
-
{ command: 'freeze-app', domain: 'app', summary: '
|
|
462
|
-
{ command: 'thaw-app', domain: 'app', summary: 'Resume target app processes with SIGCONT before
|
|
463
|
+
{ command: 'freeze-app', domain: 'app', summary: 'Optionally stop target app processes with SIGSTOP when dynamic UI needs stable evidence.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
|
|
464
|
+
{ command: 'thaw-app', domain: 'app', summary: 'Resume target app processes with SIGCONT before reads, waits, captures, actions, or final handoff.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
|
|
463
465
|
{ 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'] },
|
|
464
466
|
{ 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'] },
|
|
465
467
|
{ command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
|
|
@@ -498,8 +500,23 @@ const commandDefinitions = [
|
|
|
498
500
|
{ command: 'flutter-h5-scroll', domain: 'webview', summary: 'Scroll Flutter H5 content or a DOM element into view.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'deltaX', 'deltaY'] },
|
|
499
501
|
{ command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
|
|
500
502
|
{ command: 'webview-network', domain: 'webview', summary: 'Capture WebView Network events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'urlFilter', 'durationMs', 'script', 'includeResponseBody', 'bodyMaxBytes', 'maxEvents'] },
|
|
501
|
-
{ command: 'webview-console', domain: 'webview', summary: 'Capture WebView console/log events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'durationMs', 'script', 'maxEvents'] },
|
|
502
|
-
{ command: '
|
|
503
|
+
{ command: 'webview-console', domain: 'webview', summary: 'Capture WebView console/log events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'durationMs', 'script', 'maxEvents'] },
|
|
504
|
+
{ command: 'web-provider-status', domain: 'web', summary: 'Read desktop Web Bridge provider status.', options: [] },
|
|
505
|
+
{ command: 'web-session-start', domain: 'web', summary: 'Start the desktop Web Bridge WebSocket session server.', options: ['host', 'webPort', 'path', 'token'] },
|
|
506
|
+
{ command: 'web-connect-info', domain: 'web', summary: 'Read the Web Bridge endpoint and token for SDK clients.', options: [] },
|
|
507
|
+
{ command: 'web-sessions', domain: 'web', summary: 'List connected Web Bridge SDK sessions.', options: [] },
|
|
508
|
+
{ command: 'web-status', domain: 'web', summary: 'Read status and capture counts for a Web Bridge session.', targetKind: 'web-target', options: ['sessionId'] },
|
|
509
|
+
{ command: 'web-dom', domain: 'web', summary: 'Read or refresh a Web Bridge DOM snapshot.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'refresh', 'timeoutMs'] },
|
|
510
|
+
{ command: 'web-logs', domain: 'web', summary: 'Read Web Bridge log records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
|
|
511
|
+
{ command: 'web-network', domain: 'web', summary: 'Read Web Bridge network records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
|
|
512
|
+
{ command: 'web-state', domain: 'web', summary: 'Read Web Bridge state records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
|
|
513
|
+
{ command: 'web-events', domain: 'web', summary: 'Read Web Bridge event records.', targetKind: 'web-target', options: ['sessionId', 'sinceId', 'sinceMs', 'limit'] },
|
|
514
|
+
{ command: 'web-command', domain: 'web', summary: 'Run a whitelisted command in a connected Web Bridge SDK session.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'name', 'arguments', 'timeoutMs'] },
|
|
515
|
+
{ command: 'web-click', domain: 'web', summary: 'Click a DOM element through the Web Bridge SDK command path.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'targetText', 'timeoutMs'] },
|
|
516
|
+
{ command: 'web-input', domain: 'web', summary: 'Set text in a DOM input through the Web Bridge SDK command path.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'value', 'timeoutMs'] },
|
|
517
|
+
{ command: 'web-wait', domain: 'web', summary: 'Wait for text or selector through the Web Bridge SDK command path.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'targetText', 'timeoutMs'] },
|
|
518
|
+
{ command: 'web-scroll', domain: 'web', summary: 'Scroll a Web Bridge DOM target.', targetKind: 'web-target', options: ['sessionId', 'targetId', 'selector', 'deltaX', 'deltaY', 'timeoutMs'] },
|
|
519
|
+
{ command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
503
520
|
{ command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
|
|
504
521
|
{ command: 'batch', domain: 'advanced', summary: 'Run multiple AI App Bridge commands serially in one MCP call.', options: ['defaults', 'steps', 'stopOnError', 'includeRaw', 'maxRawChars'] },
|
|
505
522
|
{ command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
|
|
@@ -589,14 +606,15 @@ function capabilityPayload(args = {}) {
|
|
|
589
606
|
};
|
|
590
607
|
}
|
|
591
608
|
|
|
592
|
-
function shapeCommandDefinition(definition, includeOptions) {
|
|
593
|
-
return {
|
|
594
|
-
command: definition.command,
|
|
595
|
-
summary: definition.summary,
|
|
596
|
-
targetApp: Boolean(definition.targetApp),
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
609
|
+
function shapeCommandDefinition(definition, includeOptions) {
|
|
610
|
+
return {
|
|
611
|
+
command: definition.command,
|
|
612
|
+
summary: definition.summary,
|
|
613
|
+
targetApp: Boolean(definition.targetApp),
|
|
614
|
+
targetKind: definition.targetKind || (definition.targetApp ? 'android-app' : 'none'),
|
|
615
|
+
...(includeOptions ? { options: definition.options || [] } : {}),
|
|
616
|
+
};
|
|
617
|
+
}
|
|
600
618
|
|
|
601
619
|
async function runGeneric(args = {}) {
|
|
602
620
|
const command = normalizeCommandName(args.command);
|
|
@@ -606,10 +624,10 @@ async function runGeneric(args = {}) {
|
|
|
606
624
|
const commandArgs = {
|
|
607
625
|
...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
|
|
608
626
|
};
|
|
609
|
-
for (const key of ['adb', 'serial', 'port', 'packageName']) {
|
|
610
|
-
if (args[key] !== undefined && commandArgs[key] === undefined) {
|
|
611
|
-
commandArgs[key] = args[key];
|
|
612
|
-
}
|
|
627
|
+
for (const key of ['adb', 'serial', 'port', 'packageName', 'sessionId', 'targetId', 'webPort']) {
|
|
628
|
+
if (args[key] !== undefined && commandArgs[key] === undefined) {
|
|
629
|
+
commandArgs[key] = args[key];
|
|
630
|
+
}
|
|
613
631
|
}
|
|
614
632
|
if (command === 'batch') {
|
|
615
633
|
return runBatch(commandArgs);
|
|
@@ -621,19 +639,27 @@ function normalizeCommandName(value) {
|
|
|
621
639
|
return String(value || '').trim().replace(/_/g, '-');
|
|
622
640
|
}
|
|
623
641
|
|
|
624
|
-
function runBridgeChecked(command, args = {}) {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
642
|
+
function runBridgeChecked(command, args = {}) {
|
|
643
|
+
const definition = commandByName.get(command);
|
|
644
|
+
if (definition?.domain === 'web') {
|
|
645
|
+
return runWebChecked(command, args);
|
|
646
|
+
}
|
|
647
|
+
if (command === 'clear-app-data' && !args.packageName) {
|
|
648
|
+
return toolText('clear-app-data: packageName is required in MCP mode so the command cannot clear a default package.', true);
|
|
649
|
+
}
|
|
628
650
|
if ((command === 'freeze-app' || command === 'thaw-app') && !args.packageName) {
|
|
629
651
|
return toolText(`${command}: packageName is required in MCP mode so the command cannot signal a default package.`, true);
|
|
630
652
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
653
|
+
if (definition?.targetApp && !args.packageName && !args.port) {
|
|
654
|
+
return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
|
|
655
|
+
}
|
|
656
|
+
return runBridge(command, args);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function runWebChecked(command, args = {}) {
|
|
660
|
+
const result = await webProvider.run(command, args);
|
|
661
|
+
return toolJson(result, result?.ok === false);
|
|
662
|
+
}
|
|
637
663
|
|
|
638
664
|
async function runBatch(args = {}, runner = runBridgeChecked) {
|
|
639
665
|
const startedAtMs = Date.now();
|
|
@@ -654,10 +680,10 @@ async function runBatch(args = {}, runner = runBridgeChecked) {
|
|
|
654
680
|
}
|
|
655
681
|
|
|
656
682
|
const defaults = args.defaults && typeof args.defaults === 'object' ? { ...args.defaults } : {};
|
|
657
|
-
for (const key of ['adb', 'serial', 'port', 'packageName', 'artifactDir']) {
|
|
658
|
-
if (args[key] !== undefined && defaults[key] === undefined) {
|
|
659
|
-
defaults[key] = args[key];
|
|
660
|
-
}
|
|
683
|
+
for (const key of ['adb', 'serial', 'port', 'packageName', 'artifactDir', 'sessionId', 'targetId', 'webPort']) {
|
|
684
|
+
if (args[key] !== undefined && defaults[key] === undefined) {
|
|
685
|
+
defaults[key] = args[key];
|
|
686
|
+
}
|
|
661
687
|
}
|
|
662
688
|
|
|
663
689
|
const normalizedSteps = [];
|
|
@@ -706,10 +732,10 @@ async function runBatch(args = {}, runner = runBridgeChecked) {
|
|
|
706
732
|
...defaults,
|
|
707
733
|
...(step.arguments && typeof step.arguments === 'object' ? step.arguments : {}),
|
|
708
734
|
};
|
|
709
|
-
for (const key of ['adb', 'serial', 'port', 'packageName']) {
|
|
710
|
-
if (step[key] !== undefined) {
|
|
711
|
-
stepArgs[key] = step[key];
|
|
712
|
-
}
|
|
735
|
+
for (const key of ['adb', 'serial', 'port', 'packageName', 'sessionId', 'targetId', 'webPort']) {
|
|
736
|
+
if (step[key] !== undefined) {
|
|
737
|
+
stepArgs[key] = step[key];
|
|
738
|
+
}
|
|
713
739
|
}
|
|
714
740
|
try {
|
|
715
741
|
const toolResult = await runner(step.command, stepArgs);
|
|
@@ -717,11 +743,13 @@ async function runBatch(args = {}, runner = runBridgeChecked) {
|
|
|
717
743
|
const passed = !parsed.isError && parsed.payload?.ok !== false;
|
|
718
744
|
const stepResult = {
|
|
719
745
|
id: step.id,
|
|
720
|
-
command: step.command,
|
|
721
|
-
status: passed ? 'passed' : 'failed',
|
|
722
|
-
ok: passed,
|
|
723
|
-
packageName: stepArgs.packageName,
|
|
724
|
-
|
|
746
|
+
command: step.command,
|
|
747
|
+
status: passed ? 'passed' : 'failed',
|
|
748
|
+
ok: passed,
|
|
749
|
+
packageName: stepArgs.packageName,
|
|
750
|
+
sessionId: stepArgs.sessionId,
|
|
751
|
+
targetId: stepArgs.targetId,
|
|
752
|
+
port: stepArgs.port,
|
|
725
753
|
durationMs: Date.now() - stepStartedAtMs,
|
|
726
754
|
summary: summarizeToolPayload(parsed),
|
|
727
755
|
};
|
|
@@ -801,8 +829,11 @@ function summarizeToolPayload(parsed) {
|
|
|
801
829
|
ok: payload.ok,
|
|
802
830
|
error: payload.error || null,
|
|
803
831
|
};
|
|
804
|
-
if (payload.packageName) summary.packageName = payload.packageName;
|
|
805
|
-
if (payload.
|
|
832
|
+
if (payload.packageName) summary.packageName = payload.packageName;
|
|
833
|
+
if (payload.sessionId) summary.sessionId = payload.sessionId;
|
|
834
|
+
if (payload.targetId) summary.targetId = payload.targetId;
|
|
835
|
+
if (payload.session?.sessionId) summary.sessionId = payload.session.sessionId;
|
|
836
|
+
if (payload.app?.packageName) summary.app = payload.app.packageName;
|
|
806
837
|
if (payload.activity) summary.activity = payload.activity;
|
|
807
838
|
if (payload.component) summary.component = payload.component;
|
|
808
839
|
if (payload.transport) summary.transport = payload.transport;
|
|
@@ -814,8 +845,9 @@ function summarizeToolPayload(parsed) {
|
|
|
814
845
|
port: payload.debugBridge.port,
|
|
815
846
|
};
|
|
816
847
|
}
|
|
817
|
-
if (payload.count !== undefined) summary.count = payload.count;
|
|
818
|
-
if (payload.
|
|
848
|
+
if (payload.count !== undefined) summary.count = payload.count;
|
|
849
|
+
if (payload.sessionCount !== undefined) summary.sessionCount = payload.sessionCount;
|
|
850
|
+
if (payload.nodeCount !== undefined) summary.nodeCount = payload.nodeCount;
|
|
819
851
|
if (Array.isArray(payload.items)) summary.items = payload.items.length;
|
|
820
852
|
if (payload.values && typeof payload.values === 'object') {
|
|
821
853
|
summary.values = Object.keys(payload.values).length;
|