@mobileaidev/ai-app-bridge 0.2.6 → 0.2.8
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 +85 -65
- package/bin/ai-app-bridge.js +561 -465
- package/bin/mcp-server.js +755 -741
- package/package.json +7 -6
- package/skills/ai-app-bridge-use/SKILL.md +183 -0
- package/skills/ai-app-bridge-use/agents/openai.yaml +4 -0
package/bin/mcp-server.js
CHANGED
|
@@ -4,101 +4,102 @@ const { spawn } = require('child_process');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
|
|
6
6
|
const packageInfo = require('../package.json');
|
|
7
|
-
const bridgeDir = __dirname;
|
|
8
|
-
const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
|
|
9
|
-
const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
|
|
10
|
-
const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
|
|
11
|
-
const defaultProtocolVersion = supportedProtocolVersions[0];
|
|
12
|
-
const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
|
|
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, data reset, launch, and permissions.',
|
|
15
|
-
'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
7
|
+
const bridgeDir = __dirname;
|
|
8
|
+
const cliScript = path.join(bridgeDir, 'ai-app-bridge.js');
|
|
9
|
+
const nodeBinary = process.env.AI_APP_BRIDGE_NODE || process.execPath;
|
|
10
|
+
const supportedProtocolVersions = ['2025-06-18', '2024-11-05'];
|
|
11
|
+
const defaultProtocolVersion = supportedProtocolVersions[0];
|
|
12
|
+
const mcpSurface = (process.env.AI_APP_BRIDGE_MCP_SURFACE || 'compact').toLowerCase();
|
|
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, data reset, launch, and permissions.',
|
|
15
|
+
'Default surface is compact: call capabilities to discover domains, then call run with a command and arguments.',
|
|
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
|
+
'When freezing is available, keep the target app frozen while the model thinks or plans. Thaw only immediately before reading app content or performing an app action, freeze again as soon as that evidence/action result is captured, and thaw once more before finishing the overall task so the app is not left frozen.',
|
|
18
|
+
].join(' ');
|
|
19
|
+
|
|
20
|
+
let buffer = Buffer.alloc(0);
|
|
21
|
+
let responseFormat = null;
|
|
22
|
+
|
|
23
|
+
function startServer() {
|
|
24
|
+
process.stdin.on('data', (chunk) => {
|
|
25
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
26
|
+
drainMessages();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
process.stdin.on('error', () => {});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function drainMessages() {
|
|
33
|
+
while (true) {
|
|
34
|
+
const parsed = readNextMessage(buffer);
|
|
35
|
+
if (!parsed) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
buffer = parsed.remaining;
|
|
39
|
+
setResponseFormat(parsed.format);
|
|
40
|
+
handleMessage(parsed.body).catch((error) => {
|
|
41
|
+
writeLog(`unhandled message error: ${error.stack || error}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readNextMessage(source) {
|
|
47
|
+
const text = source.toString('utf8');
|
|
48
|
+
if (/^Content-Length:/i.test(text)) {
|
|
49
|
+
return readContentLengthMessage(source);
|
|
50
|
+
}
|
|
51
|
+
return readLineJsonMessage(source);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readContentLengthMessage(source) {
|
|
55
|
+
const delimiter = findHeaderDelimiter(source);
|
|
56
|
+
const headerEnd = delimiter.index;
|
|
57
|
+
if (headerEnd < 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const header = source.subarray(0, headerEnd).toString('utf8');
|
|
61
|
+
const match = /^Content-Length:\s*(\d+)$/im.exec(header);
|
|
62
|
+
if (!match) {
|
|
63
|
+
return {
|
|
64
|
+
body: source.subarray(headerEnd + delimiter.length).toString('utf8'),
|
|
65
|
+
format: 'frame',
|
|
66
|
+
remaining: Buffer.alloc(0),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const contentLength = Number(match[1]);
|
|
70
|
+
const messageStart = headerEnd + delimiter.length;
|
|
71
|
+
const messageEnd = messageStart + contentLength;
|
|
72
|
+
if (source.length < messageEnd) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
body: source.subarray(messageStart, messageEnd).toString('utf8'),
|
|
77
|
+
format: 'frame',
|
|
78
|
+
remaining: source.subarray(messageEnd),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readLineJsonMessage(source) {
|
|
83
|
+
const lfIndex = source.indexOf('\n');
|
|
84
|
+
if (lfIndex < 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const lineEnd = lfIndex > 0 && source[lfIndex - 1] === 13 ? lfIndex - 1 : lfIndex;
|
|
88
|
+
const body = source.subarray(0, lineEnd).toString('utf8');
|
|
89
|
+
return {
|
|
90
|
+
body,
|
|
91
|
+
format: 'line',
|
|
92
|
+
remaining: source.subarray(lfIndex + 1),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function setResponseFormat(format) {
|
|
97
|
+
if (!responseFormat) {
|
|
98
|
+
responseFormat = format;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findHeaderDelimiter(source) {
|
|
102
103
|
const crlfIndex = source.indexOf('\r\n\r\n');
|
|
103
104
|
const lfIndex = source.indexOf('\n\n');
|
|
104
105
|
if (crlfIndex < 0) {
|
|
@@ -124,27 +125,27 @@ async function handleMessage(body) {
|
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
try {
|
|
127
|
-
if (message.method === 'initialize') {
|
|
128
|
-
sendResult(message.id, {
|
|
129
|
-
protocolVersion: negotiateProtocolVersion(message.params?.protocolVersion),
|
|
130
|
-
capabilities: {
|
|
131
|
-
tools: {},
|
|
132
|
-
},
|
|
133
|
-
serverInfo: {
|
|
134
|
-
name: 'ai-app-bridge',
|
|
135
|
-
title: 'AI App Bridge',
|
|
136
|
-
version: packageInfo.version,
|
|
137
|
-
},
|
|
138
|
-
instructions: serverInstructions,
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (message.method === 'ping') {
|
|
144
|
-
sendResult(message.id, {});
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
128
|
+
if (message.method === 'initialize') {
|
|
129
|
+
sendResult(message.id, {
|
|
130
|
+
protocolVersion: negotiateProtocolVersion(message.params?.protocolVersion),
|
|
131
|
+
capabilities: {
|
|
132
|
+
tools: {},
|
|
133
|
+
},
|
|
134
|
+
serverInfo: {
|
|
135
|
+
name: 'ai-app-bridge',
|
|
136
|
+
title: 'AI App Bridge',
|
|
137
|
+
version: packageInfo.version,
|
|
138
|
+
},
|
|
139
|
+
instructions: serverInstructions,
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (message.method === 'ping') {
|
|
145
|
+
sendResult(message.id, {});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
148
149
|
if (message.method === 'tools/list') {
|
|
149
150
|
sendResult(message.id, { tools: toolDefinitions() });
|
|
150
151
|
return;
|
|
@@ -162,62 +163,62 @@ async function handleMessage(body) {
|
|
|
162
163
|
} catch (error) {
|
|
163
164
|
sendError(message.id, -32000, error.message || String(error));
|
|
164
165
|
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function negotiateProtocolVersion(requestedVersion) {
|
|
168
|
-
if (supportedProtocolVersions.includes(requestedVersion)) {
|
|
169
|
-
return requestedVersion;
|
|
170
|
-
}
|
|
171
|
-
return defaultProtocolVersion;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function toolDefinitions() {
|
|
175
|
-
if (mcpSurface === 'full' || mcpSurface === 'legacy') {
|
|
176
|
-
return fullToolDefinitions();
|
|
177
|
-
}
|
|
178
|
-
return compactToolDefinitions();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function compactToolDefinitions() {
|
|
182
|
-
return [
|
|
183
|
-
bridgeTool('capabilities', 'List AI App Bridge capability domains and commands. Call this first when planning Android app automation; then use run to execute the selected command.', {
|
|
184
|
-
domain: { type: 'string', description: 'Optional domain filter such as core, app, action, flutter, webview, or diagnostics.' },
|
|
185
|
-
command: { type: 'string', description: 'Optional command name for detailed arguments, such as install-apk, launch-app, tree, input-text, or webview-network.' },
|
|
186
|
-
includeOptions: { type: 'boolean', description: 'Include per-command argument names. Defaults to false to keep output compact.' },
|
|
187
|
-
}),
|
|
188
|
-
bridgeTool('run', 'Run an AI App Bridge command. Use capabilities first to choose the command. Always pass packageName for app-specific commands.', {
|
|
189
|
-
command: { type: 'string', description: 'Command name from capabilities, using CLI form such as status, install-apk, launch-app, input-text, tree, webview-network, or logcat.' },
|
|
190
|
-
packageName: { type: 'string', description: 'Target Android package for app-specific commands. Strongly recommended.' },
|
|
191
|
-
serial: { type: 'string', description: 'ADB serial when multiple devices are connected.' },
|
|
192
|
-
port: { type: 'number', description: 'Explicit bridge port when packageName discovery is not available.' },
|
|
193
|
-
adb: { type: 'string', description: 'ADB executable path or command.' },
|
|
194
|
-
arguments: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
description: 'Command-specific arguments from capabilities. Example: {"apkPath":"app-debug.apk","allowDowngrade":true}.',
|
|
197
|
-
additionalProperties: true,
|
|
198
|
-
},
|
|
199
|
-
}, ['command']),
|
|
200
|
-
];
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function fullToolDefinitions() {
|
|
204
|
-
return [
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function negotiateProtocolVersion(requestedVersion) {
|
|
169
|
+
if (supportedProtocolVersions.includes(requestedVersion)) {
|
|
170
|
+
return requestedVersion;
|
|
171
|
+
}
|
|
172
|
+
return defaultProtocolVersion;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toolDefinitions() {
|
|
176
|
+
if (mcpSurface === 'full' || mcpSurface === 'legacy') {
|
|
177
|
+
return fullToolDefinitions();
|
|
178
|
+
}
|
|
179
|
+
return compactToolDefinitions();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function compactToolDefinitions() {
|
|
183
|
+
return [
|
|
184
|
+
bridgeTool('capabilities', 'List AI App Bridge capability domains and commands. Call this first when planning Android app automation; then use run to execute the selected command.', {
|
|
185
|
+
domain: { type: 'string', description: 'Optional domain filter such as core, app, action, flutter, webview, or diagnostics.' },
|
|
186
|
+
command: { type: 'string', description: 'Optional command name for detailed arguments, such as install-apk, launch-app, tree, input-text, or webview-network.' },
|
|
187
|
+
includeOptions: { type: 'boolean', description: 'Include per-command argument names. Defaults to false to keep output compact.' },
|
|
188
|
+
}),
|
|
189
|
+
bridgeTool('run', 'Run an AI App Bridge command. Use capabilities first to choose the command. Always pass packageName for app-specific commands.', {
|
|
190
|
+
command: { type: 'string', description: 'Command name from capabilities, using CLI form such as status, install-apk, launch-app, freeze-app, thaw-app, input-text, tree, webview-network, or logcat.' },
|
|
191
|
+
packageName: { type: 'string', description: 'Target Android package for app-specific commands. Strongly recommended.' },
|
|
192
|
+
serial: { type: 'string', description: 'ADB serial when multiple devices are connected.' },
|
|
193
|
+
port: { type: 'number', description: 'Explicit bridge port when packageName discovery is not available.' },
|
|
194
|
+
adb: { type: 'string', description: 'ADB executable path or command.' },
|
|
195
|
+
arguments: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
description: 'Command-specific arguments from capabilities. Example: {"apkPath":"app-debug.apk","allowDowngrade":true}.',
|
|
198
|
+
additionalProperties: true,
|
|
199
|
+
},
|
|
200
|
+
}, ['command']),
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function fullToolDefinitions() {
|
|
205
|
+
return [
|
|
205
206
|
bridgeTool('status', 'Read compact bridge status, app info, capture counts, and Flutter layout summary. If default port 18080 times out, agents should retry with the target Android packageName so the CLI can discover the app bridge port.', {
|
|
206
207
|
full: { type: 'boolean', description: 'Return the full raw status payload, including large Flutter widget dumps.' },
|
|
207
208
|
}),
|
|
208
|
-
bridgeTool('tree', 'Read the Android View tree from the in-app bridge.'),
|
|
209
|
-
bridgeTool('flutter_tree', 'Read the latest Flutter widget/layout snapshot.'),
|
|
210
|
-
bridgeTool('flutter_nodes', 'Read Flutter operable nodes from the Flutter action bridge.'),
|
|
211
|
-
bridgeTool('tap_flutter_text', 'Tap a Flutter node by visible text through the Flutter-aware bridge path.', {
|
|
212
|
-
targetText: { type: 'string', description: 'Flutter node text to tap.' },
|
|
213
|
-
}, ['targetText']),
|
|
214
|
-
bridgeTool('input_flutter_text', 'Set Flutter TextField text through the Flutter action bridge. Use this for Flutter Chinese/Unicode input; do not use raw adb shell input text.', {
|
|
215
|
-
text: { type: 'string', description: 'Text to set.' },
|
|
216
|
-
tapX: { type: 'number', description: 'Optional physical X coordinate for the Flutter input target.' },
|
|
217
|
-
tapY: { type: 'number', description: 'Optional physical Y coordinate for the Flutter input target.' },
|
|
218
|
-
hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
|
|
219
|
-
}, ['text']),
|
|
220
|
-
bridgeTool('h5_dom', 'Read native Android WebView DOM from the current Activity.'),
|
|
209
|
+
bridgeTool('tree', 'Read the Android View tree from the in-app bridge.'),
|
|
210
|
+
bridgeTool('flutter_tree', 'Read the latest Flutter widget/layout snapshot.'),
|
|
211
|
+
bridgeTool('flutter_nodes', 'Read Flutter operable nodes from the Flutter action bridge.'),
|
|
212
|
+
bridgeTool('tap_flutter_text', 'Tap a Flutter node by visible text through the Flutter-aware bridge path.', {
|
|
213
|
+
targetText: { type: 'string', description: 'Flutter node text to tap.' },
|
|
214
|
+
}, ['targetText']),
|
|
215
|
+
bridgeTool('input_flutter_text', 'Set Flutter TextField text through the Flutter action bridge. Use this for Flutter Chinese/Unicode input; do not use raw adb shell input text.', {
|
|
216
|
+
text: { type: 'string', description: 'Text to set.' },
|
|
217
|
+
tapX: { type: 'number', description: 'Optional physical X coordinate for the Flutter input target.' },
|
|
218
|
+
tapY: { type: 'number', description: 'Optional physical Y coordinate for the Flutter input target.' },
|
|
219
|
+
hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
|
|
220
|
+
}, ['text']),
|
|
221
|
+
bridgeTool('h5_dom', 'Read native Android WebView DOM from the current Activity.'),
|
|
221
222
|
bridgeTool('h5_eval', 'Execute debug JavaScript in the current native Android WebView.', {
|
|
222
223
|
script: { type: 'string' },
|
|
223
224
|
}, ['script']),
|
|
@@ -256,6 +257,12 @@ function fullToolDefinitions() {
|
|
|
256
257
|
deltaY: { type: 'number', description: 'Window scroll delta Y when no selector/text is supplied.' },
|
|
257
258
|
}),
|
|
258
259
|
bridgeTool('logs', 'Read generic in-app log records.'),
|
|
260
|
+
bridgeTool('freeze_app', 'Stop the target app processes with SIGSTOP after app content is captured, preventing playback or animation from changing the observed evidence.', {
|
|
261
|
+
pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
|
|
262
|
+
}, ['packageName']),
|
|
263
|
+
bridgeTool('thaw_app', 'Resume the target app processes with SIGCONT before reading app content or dispatching actions, so bridge endpoints can answer.', {
|
|
264
|
+
pid: { type: 'string', description: 'Optional explicit process id. Defaults to all processes named packageName or packageName:*.' },
|
|
265
|
+
}, ['packageName']),
|
|
259
266
|
bridgeTool('logcat', 'Read Android logcat through ADB with optional pid/tag/level/grep filters.', {
|
|
260
267
|
pid: { type: 'string', description: 'Use "current" for the current app pid, or pass a numeric pid.' },
|
|
261
268
|
appPid: { type: 'boolean', description: 'Filter by the current package pid.' },
|
|
@@ -308,19 +315,19 @@ function fullToolDefinitions() {
|
|
|
308
315
|
bridgeTool('events', 'Read generic in-app event records.'),
|
|
309
316
|
bridgeTool('uia_tree', 'Read UIAutomator XML for the current device window.'),
|
|
310
317
|
bridgeTool('screenshot', 'Capture an ADB screenshot.'),
|
|
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']),
|
|
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()),
|
|
321
|
-
bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
|
|
322
|
-
bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
|
|
323
|
-
bridgeTool('launch_flutter', 'Launch the Flutter Activity, optionally with an initial route.'),
|
|
318
|
+
bridgeTool('install_apk', 'Install an APK through ADB while assisting device-side package-installer confirmation screens with UIAutomator.', {
|
|
319
|
+
apkPath: { type: 'string', description: 'Absolute or workspace-relative APK path.' },
|
|
320
|
+
allowDowngrade: { type: 'boolean', description: 'Pass -d to adb install.' },
|
|
321
|
+
streaming: { type: 'boolean', description: 'Use streaming install instead of the default --no-streaming mode.' },
|
|
322
|
+
installTimeoutMs: { type: 'number', description: 'Maximum time for adb install. Defaults to 180000 ms.' },
|
|
323
|
+
installerTimeoutMs: { type: 'number', description: 'Maximum time to keep assisting installer screens after adb install exits. Defaults to 90000 ms.' },
|
|
324
|
+
intervalMs: { type: 'number', description: 'Installer polling interval. Defaults to 700 ms.' },
|
|
325
|
+
}, ['apkPath']),
|
|
326
|
+
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']),
|
|
327
|
+
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()),
|
|
328
|
+
bridgeTool('launch_activity', 'Launch an explicit Android Activity component, optionally with action/data/category/string extras.', launchProperties()),
|
|
329
|
+
bridgeTool('launch_native_test', 'Launch the debug native Android bridge test Activity.'),
|
|
330
|
+
bridgeTool('launch_flutter', 'Launch the Flutter Activity, optionally with an initial route.'),
|
|
324
331
|
bridgeTool('tap', 'Tap device coordinates through ADB.', {
|
|
325
332
|
tapX: { type: 'number' },
|
|
326
333
|
tapY: { type: 'number' },
|
|
@@ -333,12 +340,12 @@ function fullToolDefinitions() {
|
|
|
333
340
|
targetText: { type: 'string' },
|
|
334
341
|
timeoutSec: { type: 'number' },
|
|
335
342
|
}, ['targetText']),
|
|
336
|
-
bridgeTool('input_text', 'Set native Android text through the in-app bridge. Use this for Chinese/Unicode; always pass packageName so the tool targets the intended app.', {
|
|
337
|
-
text: { type: 'string', description: 'Text to set in the focused or coordinate-matched native EditText.' },
|
|
338
|
-
tapX: { type: 'number', description: 'Optional X coordinate used to choose a native EditText target.' },
|
|
339
|
-
tapY: { type: 'number', description: 'Optional Y coordinate used to choose a native EditText target.' },
|
|
340
|
-
hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
|
|
341
|
-
}, ['text', 'packageName']),
|
|
343
|
+
bridgeTool('input_text', 'Set native Android text through the in-app bridge. Use this for Chinese/Unicode; always pass packageName so the tool targets the intended app.', {
|
|
344
|
+
text: { type: 'string', description: 'Text to set in the focused or coordinate-matched native EditText.' },
|
|
345
|
+
tapX: { type: 'number', description: 'Optional X coordinate used to choose a native EditText target.' },
|
|
346
|
+
tapY: { type: 'number', description: 'Optional Y coordinate used to choose a native EditText target.' },
|
|
347
|
+
hideKeyboard: { type: 'boolean', description: 'Hide the soft keyboard after setting text.' },
|
|
348
|
+
}, ['text', 'packageName']),
|
|
342
349
|
bridgeTool('keyboard_state', 'Read Android soft-keyboard visibility from dumpsys input_method.'),
|
|
343
350
|
bridgeTool('hide_keyboard', 'Hide the Android soft keyboard when it is visible.', {
|
|
344
351
|
force: { type: 'boolean', description: 'Send keyboard-dismiss keys even when the visibility probe says the keyboard is hidden.' },
|
|
@@ -387,29 +394,29 @@ function fullToolDefinitions() {
|
|
|
387
394
|
];
|
|
388
395
|
}
|
|
389
396
|
|
|
390
|
-
function bridgeTool(name, description, properties = {}, required = []) {
|
|
391
|
-
return {
|
|
392
|
-
name,
|
|
393
|
-
description,
|
|
394
|
-
inputSchema: baseSchema(properties, required),
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function launchProperties() {
|
|
399
|
-
return {
|
|
400
|
-
activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
|
|
401
|
-
component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
|
|
402
|
-
action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
|
|
403
|
-
category: { type: 'string', description: 'Intent category.' },
|
|
404
|
-
data: { type: 'string', description: 'Intent data URI.' },
|
|
405
|
-
extra: {
|
|
406
|
-
type: 'object',
|
|
407
|
-
additionalProperties: { type: 'string' },
|
|
408
|
-
description: 'String intent extras as an object of key/value pairs.',
|
|
409
|
-
},
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
397
|
+
function bridgeTool(name, description, properties = {}, required = []) {
|
|
398
|
+
return {
|
|
399
|
+
name,
|
|
400
|
+
description,
|
|
401
|
+
inputSchema: baseSchema(properties, required),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function launchProperties() {
|
|
406
|
+
return {
|
|
407
|
+
activity: { type: 'string', description: 'Activity class, such as .MainActivity or com.example.MainActivity.' },
|
|
408
|
+
component: { type: 'string', description: 'Explicit Android component, such as com.example/.MainActivity.' },
|
|
409
|
+
action: { type: 'string', description: 'Intent action for explicit Activity launch.' },
|
|
410
|
+
category: { type: 'string', description: 'Intent category.' },
|
|
411
|
+
data: { type: 'string', description: 'Intent data URI.' },
|
|
412
|
+
extra: {
|
|
413
|
+
type: 'object',
|
|
414
|
+
additionalProperties: { type: 'string' },
|
|
415
|
+
description: 'String intent extras as an object of key/value pairs.',
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
413
420
|
function baseSchema(extraProperties = {}, extraRequired = []) {
|
|
414
421
|
return {
|
|
415
422
|
type: 'object',
|
|
@@ -431,84 +438,86 @@ function baseSchema(extraProperties = {}, extraRequired = []) {
|
|
|
431
438
|
};
|
|
432
439
|
}
|
|
433
440
|
|
|
434
|
-
function h5TargetSchema() {
|
|
435
|
-
return {
|
|
436
|
-
selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
|
|
441
|
+
function h5TargetSchema() {
|
|
442
|
+
return {
|
|
443
|
+
selector: { type: 'string', description: 'CSS selector for the target DOM element.' },
|
|
437
444
|
targetText: { type: 'string', description: 'Text, value, aria-label, placeholder, id, name, or role to match.' },
|
|
438
445
|
exact: { type: 'boolean', description: 'Require exact text match instead of substring match.' },
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const commandDefinitions = [
|
|
443
|
-
{ command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
|
|
444
|
-
{ command: 'tree', domain: 'core', summary: 'Read Android View tree from the in-app bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes', 'maxDepth'] },
|
|
445
|
-
{ command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes'] },
|
|
446
|
-
{ command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
|
|
447
|
-
{ command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
448
|
-
{ command: 'network', domain: 'core', summary: 'Read in-app network records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'urlFilter', 'method', 'statusCode', 'noBodies', 'bodyMaxBytes', 'sinceId', 'sinceMs', 'limit'] },
|
|
449
|
-
{ command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
450
|
-
{ command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
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'] },
|
|
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'] },
|
|
454
|
-
{ command: '
|
|
455
|
-
{ command: '
|
|
456
|
-
{ command: 'launch-
|
|
457
|
-
{ command: 'launch-
|
|
458
|
-
{ command: '
|
|
459
|
-
{ command: '
|
|
460
|
-
{ command: 'permission-
|
|
461
|
-
{ command: 'permission-
|
|
462
|
-
{ command: '
|
|
463
|
-
{ command: '
|
|
464
|
-
{ command: '
|
|
465
|
-
{ command: 'tap
|
|
466
|
-
{ command: '
|
|
467
|
-
{ command: '
|
|
468
|
-
{ command: '
|
|
469
|
-
{ command: '
|
|
470
|
-
{ command: '
|
|
471
|
-
{ command: '
|
|
472
|
-
{ command: '
|
|
473
|
-
{ command: '
|
|
474
|
-
{ command: 'flutter-
|
|
475
|
-
{ command: '
|
|
476
|
-
{ command: '
|
|
477
|
-
{ command: '
|
|
478
|
-
{ command: '
|
|
479
|
-
{ command: '
|
|
480
|
-
{ command: 'h5-
|
|
481
|
-
{ command: 'h5-
|
|
482
|
-
{ command: 'h5-
|
|
483
|
-
{ command: 'h5-
|
|
484
|
-
{ command: '
|
|
485
|
-
{ command: '
|
|
486
|
-
{ command: 'flutter-h5-
|
|
487
|
-
{ command: 'flutter-h5-
|
|
488
|
-
{ command: 'flutter-h5-
|
|
489
|
-
{ command: 'flutter-h5-
|
|
490
|
-
{ command: '
|
|
491
|
-
{ command: '
|
|
492
|
-
{ command: 'webview-
|
|
493
|
-
{ command: '
|
|
494
|
-
{ command: '
|
|
495
|
-
{ command: '
|
|
496
|
-
{ command: '
|
|
497
|
-
]
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const commandDefinitions = [
|
|
450
|
+
{ command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
|
|
451
|
+
{ command: 'tree', domain: 'core', summary: 'Read Android View tree from the in-app bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes', 'maxDepth'] },
|
|
452
|
+
{ command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes'] },
|
|
453
|
+
{ command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
|
|
454
|
+
{ command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
455
|
+
{ command: 'network', domain: 'core', summary: 'Read in-app network records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'urlFilter', 'method', 'statusCode', 'noBodies', 'bodyMaxBytes', 'sinceId', 'sinceMs', 'limit'] },
|
|
456
|
+
{ command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
457
|
+
{ command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
458
|
+
{ 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
|
+
{ 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
|
+
{ 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: 'Stop target app processes with SIGSTOP after content capture to keep observed evidence stable.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
|
|
462
|
+
{ command: 'thaw-app', domain: 'app', summary: 'Resume target app processes with SIGCONT before content capture or actions so bridge data can be read.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
|
|
463
|
+
{ 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
|
+
{ 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
|
+
{ command: 'launch-native-test', domain: 'app', summary: 'Launch the debug native bridge test Activity.', targetApp: true, options: ['serial', 'packageName'] },
|
|
466
|
+
{ command: 'launch-flutter', domain: 'app', summary: 'Launch the Flutter Activity, optionally with an initial route.', targetApp: true, options: ['serial', 'packageName', 'initialRoute'] },
|
|
467
|
+
{ command: 'permission-state', domain: 'app', summary: 'Read Android runtime permission state.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
|
|
468
|
+
{ command: 'permission-grant', domain: 'app', summary: 'Grant an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
|
|
469
|
+
{ command: 'permission-revoke', domain: 'app', summary: 'Revoke an Android runtime permission.', targetApp: true, options: ['serial', 'packageName', 'permission'] },
|
|
470
|
+
{ command: 'permission-dialog', domain: 'app', summary: 'Tap a visible Android permission dialog allow button.', options: ['serial', 'targetText', 'buttonText', 'resourceId', 'attempts', 'intervalMs', 'exact'] },
|
|
471
|
+
{ command: 'appops-set', domain: 'app', summary: 'Set an Android app-op mode.', targetApp: true, options: ['serial', 'packageName', 'op', 'mode'] },
|
|
472
|
+
{ command: 'tap', domain: 'action', summary: 'Tap device coordinates through ADB.', options: ['serial', 'tapX', 'tapY'] },
|
|
473
|
+
{ command: 'tap-text', domain: 'action', summary: 'Tap a visible Android View node by text/contentDescription through the bridge tree.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'noAutoHideKeyboard'] },
|
|
474
|
+
{ command: 'tap-uia-text', domain: 'action', summary: 'Tap a UIAutomator node by text without relying on the in-app tree.', options: ['serial', 'targetText', 'exact'] },
|
|
475
|
+
{ command: 'wait-text', domain: 'action', summary: 'Wait until text appears in bridge status/tree or UIAutomator output.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'timeoutSec', 'requireText', 'absentText', 'requireActivity'] },
|
|
476
|
+
{ command: 'input-text', domain: 'action', summary: 'Set native Android text through the in-app bridge; use this for Chinese/Unicode.', targetApp: true, options: ['serial', 'packageName', 'text', 'tapX', 'tapY', 'hideKeyboard'] },
|
|
477
|
+
{ command: 'keyboard-state', domain: 'action', summary: 'Read Android soft keyboard visibility.', options: ['serial'] },
|
|
478
|
+
{ command: 'hide-keyboard', domain: 'action', summary: 'Hide the Android soft keyboard.', options: ['serial', 'force', 'intervalMs'] },
|
|
479
|
+
{ command: 'swipe', domain: 'action', summary: 'Swipe device coordinates through ADB.', options: ['serial', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
|
|
480
|
+
{ command: 'keyevent', domain: 'action', summary: 'Send an Android keyevent through ADB.', options: ['serial', 'keyCode'] },
|
|
481
|
+
{ command: 'flutter-tree', domain: 'flutter', summary: 'Read the latest Flutter layout snapshot.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
482
|
+
{ command: 'flutter-nodes', domain: 'flutter', summary: 'Read Flutter operable nodes.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
483
|
+
{ command: 'flutter-action', domain: 'flutter', summary: 'Dispatch a raw Flutter action payload.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
|
|
484
|
+
{ command: 'tap-flutter-text', domain: 'flutter', summary: 'Tap a Flutter node by visible text.', targetApp: true, options: ['serial', 'packageName', 'targetText'] },
|
|
485
|
+
{ command: 'input-flutter-text', domain: 'flutter', summary: 'Set Flutter TextField text through the Flutter action bridge.', targetApp: true, options: ['serial', 'packageName', 'text', 'tapX', 'tapY', 'hideKeyboard'] },
|
|
486
|
+
{ command: 'scroll-flutter', domain: 'flutter', summary: 'Scroll Flutter content by delta or until text is visible.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'delta', 'maxSwipes'] },
|
|
487
|
+
{ command: 'h5-dom', domain: 'webview', summary: 'Read native Android WebView DOM.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
488
|
+
{ command: 'h5-eval', domain: 'webview', summary: 'Execute JavaScript in the current native Android WebView.', targetApp: true, options: ['serial', 'packageName', 'script'] },
|
|
489
|
+
{ command: 'h5-click', domain: 'webview', summary: 'Click a native WebView element by selector or text.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
|
|
490
|
+
{ command: 'h5-input', domain: 'webview', summary: 'Set text in a native WebView input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
|
|
491
|
+
{ command: 'h5-wait', domain: 'webview', summary: 'Wait for native WebView text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
|
|
492
|
+
{ command: 'h5-scroll', domain: 'webview', summary: 'Scroll native WebView content or a DOM element into view.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'deltaX', 'deltaY'] },
|
|
493
|
+
{ command: 'flutter-h5-dom', domain: 'webview', summary: 'Read DOM through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
494
|
+
{ command: 'flutter-h5-eval', domain: 'webview', summary: 'Execute JavaScript through a Flutter H5 adapter.', targetApp: true, options: ['serial', 'packageName', 'script'] },
|
|
495
|
+
{ command: 'flutter-h5-click', domain: 'webview', summary: 'Click a Flutter H5 DOM element.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'exact'] },
|
|
496
|
+
{ command: 'flutter-h5-input', domain: 'webview', summary: 'Set text in a Flutter H5 input.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'value', 'exact'] },
|
|
497
|
+
{ command: 'flutter-h5-wait', domain: 'webview', summary: 'Wait for Flutter H5 text or selector.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'timeoutSec', 'intervalMs'] },
|
|
498
|
+
{ 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
|
+
{ command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
|
|
500
|
+
{ 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: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
503
|
+
{ command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
|
|
504
|
+
{ command: 'batch', domain: 'advanced', summary: 'Run multiple AI App Bridge commands serially in one MCP call.', options: ['defaults', 'steps', 'stopOnError', 'includeRaw', 'maxRawChars'] },
|
|
505
|
+
{ command: 'smoke', domain: 'diagnostics', summary: 'Run the native sample smoke test.', options: ['serial', 'packageName', 'outFile', 'artifactDir', 'skipFlutterLaunch'] },
|
|
506
|
+
];
|
|
507
|
+
|
|
508
|
+
const commandByName = new Map(commandDefinitions.map((definition) => [definition.command, definition]));
|
|
509
|
+
|
|
510
|
+
async function callTool(name, args) {
|
|
511
|
+
if (name === 'capabilities') {
|
|
512
|
+
return toolJson(capabilityPayload(args));
|
|
513
|
+
}
|
|
514
|
+
if (name === 'run') {
|
|
515
|
+
return runGeneric(args);
|
|
516
|
+
}
|
|
517
|
+
if (name === 'run_smoke') {
|
|
518
|
+
return runSmoke(args);
|
|
519
|
+
}
|
|
520
|
+
const commandMap = {
|
|
512
521
|
flutter_tree: 'flutter-tree',
|
|
513
522
|
h5_dom: 'h5-dom',
|
|
514
523
|
h5_eval: 'h5-eval',
|
|
@@ -516,22 +525,24 @@ async function callTool(name, args) {
|
|
|
516
525
|
h5_input: 'h5-input',
|
|
517
526
|
h5_wait: 'h5-wait',
|
|
518
527
|
h5_scroll: 'h5-scroll',
|
|
528
|
+
freeze_app: 'freeze-app',
|
|
529
|
+
thaw_app: 'thaw-app',
|
|
519
530
|
flutter_h5_dom: 'flutter-h5-dom',
|
|
520
531
|
flutter_h5_eval: 'flutter-h5-eval',
|
|
521
532
|
flutter_h5_click: 'flutter-h5-click',
|
|
522
533
|
flutter_h5_input: 'flutter-h5-input',
|
|
523
|
-
flutter_h5_wait: 'flutter-h5-wait',
|
|
524
|
-
flutter_h5_scroll: 'flutter-h5-scroll',
|
|
525
|
-
flutter_nodes: 'flutter-nodes',
|
|
526
|
-
tap_flutter_text: 'tap-flutter-text',
|
|
527
|
-
input_flutter_text: 'input-flutter-text',
|
|
528
|
-
uia_tree: 'uia-tree',
|
|
529
|
-
install_apk: 'install-apk',
|
|
530
|
-
clear_app_data: 'clear-app-data',
|
|
531
|
-
launch_app: 'launch-app',
|
|
532
|
-
launch_activity: 'launch-activity',
|
|
533
|
-
launch_native_test: 'launch-native-test',
|
|
534
|
-
launch_flutter: 'launch-flutter',
|
|
534
|
+
flutter_h5_wait: 'flutter-h5-wait',
|
|
535
|
+
flutter_h5_scroll: 'flutter-h5-scroll',
|
|
536
|
+
flutter_nodes: 'flutter-nodes',
|
|
537
|
+
tap_flutter_text: 'tap-flutter-text',
|
|
538
|
+
input_flutter_text: 'input-flutter-text',
|
|
539
|
+
uia_tree: 'uia-tree',
|
|
540
|
+
install_apk: 'install-apk',
|
|
541
|
+
clear_app_data: 'clear-app-data',
|
|
542
|
+
launch_app: 'launch-app',
|
|
543
|
+
launch_activity: 'launch-activity',
|
|
544
|
+
launch_native_test: 'launch-native-test',
|
|
545
|
+
launch_flutter: 'launch-flutter',
|
|
535
546
|
tap_text: 'tap-text',
|
|
536
547
|
wait_text: 'wait-text',
|
|
537
548
|
input_text: 'input-text',
|
|
@@ -546,316 +557,319 @@ async function callTool(name, args) {
|
|
|
546
557
|
appops_set: 'appops-set',
|
|
547
558
|
tap_uia_text: 'tap-uia-text',
|
|
548
559
|
permission_dialog: 'permission-dialog',
|
|
549
|
-
};
|
|
550
|
-
const command = commandMap[name] || name;
|
|
551
|
-
return runBridgeChecked(command, args);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function capabilityPayload(args = {}) {
|
|
555
|
-
const includeOptions = Boolean(args.includeOptions);
|
|
556
|
-
const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
|
|
557
|
-
if (requestedCommand) {
|
|
558
|
-
const definition = commandByName.get(requestedCommand);
|
|
559
|
-
return {
|
|
560
|
-
ok: Boolean(definition),
|
|
561
|
-
command: requestedCommand,
|
|
562
|
-
...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const requestedDomain = args.domain ? String(args.domain) : '';
|
|
567
|
-
const domains = {};
|
|
568
|
-
for (const definition of commandDefinitions) {
|
|
569
|
-
if (requestedDomain && definition.domain !== requestedDomain) continue;
|
|
570
|
-
if (!domains[definition.domain]) domains[definition.domain] = [];
|
|
571
|
-
domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
|
|
572
|
-
}
|
|
573
|
-
return {
|
|
574
|
-
ok: true,
|
|
575
|
-
surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
|
|
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.',
|
|
577
|
-
domains,
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function shapeCommandDefinition(definition, includeOptions) {
|
|
582
|
-
return {
|
|
583
|
-
command: definition.command,
|
|
584
|
-
summary: definition.summary,
|
|
585
|
-
targetApp: Boolean(definition.targetApp),
|
|
586
|
-
...(includeOptions ? { options: definition.options || [] } : {}),
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
async function runGeneric(args = {}) {
|
|
591
|
-
const command = normalizeCommandName(args.command);
|
|
592
|
-
if (!commandByName.has(command)) {
|
|
593
|
-
return toolText(`unknown command: ${args.command || ''}`, true);
|
|
594
|
-
}
|
|
595
|
-
const commandArgs = {
|
|
596
|
-
...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
|
|
597
|
-
};
|
|
598
|
-
for (const key of ['adb', 'serial', 'port', 'packageName']) {
|
|
599
|
-
if (args[key] !== undefined && commandArgs[key] === undefined) {
|
|
600
|
-
commandArgs[key] = args[key];
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
if (command === 'batch') {
|
|
604
|
-
return runBatch(commandArgs);
|
|
605
|
-
}
|
|
606
|
-
return runBridgeChecked(command, commandArgs);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
function normalizeCommandName(value) {
|
|
610
|
-
return String(value || '').trim().replace(/_/g, '-');
|
|
611
|
-
}
|
|
612
|
-
|
|
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
|
-
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
return toolJson({ ok: false, error: '
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
seenIds.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (command
|
|
663
|
-
return toolJson({ ok: false, error: '
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
if (
|
|
718
|
-
stepResult.
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (payload.
|
|
794
|
-
if (payload.
|
|
795
|
-
if (payload.
|
|
796
|
-
if (payload.
|
|
797
|
-
if (payload.
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
if (payload.
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
if (payload.
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
if (payload.
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
addArg(cliArgs, '
|
|
855
|
-
|
|
856
|
-
addArg(cliArgs, '
|
|
857
|
-
|
|
858
|
-
|
|
560
|
+
};
|
|
561
|
+
const command = commandMap[name] || name;
|
|
562
|
+
return runBridgeChecked(command, args);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function capabilityPayload(args = {}) {
|
|
566
|
+
const includeOptions = Boolean(args.includeOptions);
|
|
567
|
+
const requestedCommand = args.command ? normalizeCommandName(args.command) : '';
|
|
568
|
+
if (requestedCommand) {
|
|
569
|
+
const definition = commandByName.get(requestedCommand);
|
|
570
|
+
return {
|
|
571
|
+
ok: Boolean(definition),
|
|
572
|
+
command: requestedCommand,
|
|
573
|
+
...(definition ? shapeCommandDefinition(definition, true) : { error: 'unknown_command' }),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const requestedDomain = args.domain ? String(args.domain) : '';
|
|
578
|
+
const domains = {};
|
|
579
|
+
for (const definition of commandDefinitions) {
|
|
580
|
+
if (requestedDomain && definition.domain !== requestedDomain) continue;
|
|
581
|
+
if (!domains[definition.domain]) domains[definition.domain] = [];
|
|
582
|
+
domains[definition.domain].push(shapeCommandDefinition(definition, includeOptions));
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
ok: true,
|
|
586
|
+
surface: mcpSurface === 'full' || mcpSurface === 'legacy' ? 'full' : 'compact',
|
|
587
|
+
usage: 'Use run with one of these command names. Prefer packageName for app-specific commands; install-apk, clear-app-data, launch-app, freeze/thaw, UI, WebView, logcat, network, and permission workflows are supported.',
|
|
588
|
+
domains,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function shapeCommandDefinition(definition, includeOptions) {
|
|
593
|
+
return {
|
|
594
|
+
command: definition.command,
|
|
595
|
+
summary: definition.summary,
|
|
596
|
+
targetApp: Boolean(definition.targetApp),
|
|
597
|
+
...(includeOptions ? { options: definition.options || [] } : {}),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function runGeneric(args = {}) {
|
|
602
|
+
const command = normalizeCommandName(args.command);
|
|
603
|
+
if (!commandByName.has(command)) {
|
|
604
|
+
return toolText(`unknown command: ${args.command || ''}`, true);
|
|
605
|
+
}
|
|
606
|
+
const commandArgs = {
|
|
607
|
+
...(args.arguments && typeof args.arguments === 'object' ? args.arguments : {}),
|
|
608
|
+
};
|
|
609
|
+
for (const key of ['adb', 'serial', 'port', 'packageName']) {
|
|
610
|
+
if (args[key] !== undefined && commandArgs[key] === undefined) {
|
|
611
|
+
commandArgs[key] = args[key];
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (command === 'batch') {
|
|
615
|
+
return runBatch(commandArgs);
|
|
616
|
+
}
|
|
617
|
+
return runBridgeChecked(command, commandArgs);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function normalizeCommandName(value) {
|
|
621
|
+
return String(value || '').trim().replace(/_/g, '-');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function runBridgeChecked(command, args = {}) {
|
|
625
|
+
if (command === 'clear-app-data' && !args.packageName) {
|
|
626
|
+
return toolText('clear-app-data: packageName is required in MCP mode so the command cannot clear a default package.', true);
|
|
627
|
+
}
|
|
628
|
+
if ((command === 'freeze-app' || command === 'thaw-app') && !args.packageName) {
|
|
629
|
+
return toolText(`${command}: packageName is required in MCP mode so the command cannot signal a default package.`, true);
|
|
630
|
+
}
|
|
631
|
+
const definition = commandByName.get(command);
|
|
632
|
+
if (definition?.targetApp && !args.packageName && !args.port) {
|
|
633
|
+
return toolText(`${command}: packageName or explicit port is required in MCP mode so the command cannot fall back to a default package.`, true);
|
|
634
|
+
}
|
|
635
|
+
return runBridge(command, args);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function runBatch(args = {}, runner = runBridgeChecked) {
|
|
639
|
+
const startedAtMs = Date.now();
|
|
640
|
+
const mode = args.mode ? String(args.mode) : 'serial';
|
|
641
|
+
if (mode !== 'serial') {
|
|
642
|
+
return toolJson({ ok: false, error: 'batch_mode_not_supported', mode }, true);
|
|
643
|
+
}
|
|
644
|
+
const steps = Array.isArray(args.steps) ? args.steps : [];
|
|
645
|
+
if (steps.length === 0) {
|
|
646
|
+
return toolJson({ ok: false, error: 'batch_steps_required' }, true);
|
|
647
|
+
}
|
|
648
|
+
const maxSteps = args.maxSteps === undefined ? 30 : Number(args.maxSteps);
|
|
649
|
+
if (!Number.isInteger(maxSteps) || maxSteps < 1) {
|
|
650
|
+
return toolJson({ ok: false, error: 'invalid_max_steps', maxSteps: args.maxSteps }, true);
|
|
651
|
+
}
|
|
652
|
+
if (steps.length > maxSteps) {
|
|
653
|
+
return toolJson({ ok: false, error: 'batch_too_many_steps', stepCount: steps.length, maxSteps }, true);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
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
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const normalizedSteps = [];
|
|
664
|
+
const seenIds = new Set();
|
|
665
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
666
|
+
const rawStep = steps[index] && typeof steps[index] === 'object' ? steps[index] : {};
|
|
667
|
+
const stepId = String(rawStep.id || `step_${index + 1}`);
|
|
668
|
+
if (seenIds.has(stepId)) {
|
|
669
|
+
return toolJson({ ok: false, error: 'duplicate_batch_step_id', stepId }, true);
|
|
670
|
+
}
|
|
671
|
+
seenIds.add(stepId);
|
|
672
|
+
const command = normalizeCommandName(rawStep.command);
|
|
673
|
+
if (!commandByName.has(command)) {
|
|
674
|
+
return toolJson({ ok: false, error: 'unknown_batch_step_command', stepId, command: rawStep.command || '' }, true);
|
|
675
|
+
}
|
|
676
|
+
if (command === 'batch') {
|
|
677
|
+
return toolJson({ ok: false, error: 'nested_batch_not_supported', stepId }, true);
|
|
678
|
+
}
|
|
679
|
+
normalizedSteps.push({ ...rawStep, id: stepId, command });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const stopOnError = args.stopOnError !== false;
|
|
683
|
+
const includeRaw = Boolean(args.includeRaw);
|
|
684
|
+
const maxRawChars = args.maxRawChars === undefined ? 4000 : Number(args.maxRawChars);
|
|
685
|
+
if (!Number.isInteger(maxRawChars) || maxRawChars < 0) {
|
|
686
|
+
return toolJson({ ok: false, error: 'invalid_max_raw_chars', maxRawChars: args.maxRawChars }, true);
|
|
687
|
+
}
|
|
688
|
+
const results = [];
|
|
689
|
+
let stopped = false;
|
|
690
|
+
|
|
691
|
+
for (const step of normalizedSteps) {
|
|
692
|
+
if (stopped) {
|
|
693
|
+
results.push({
|
|
694
|
+
id: step.id,
|
|
695
|
+
command: step.command,
|
|
696
|
+
status: 'skipped',
|
|
697
|
+
ok: false,
|
|
698
|
+
skipped: true,
|
|
699
|
+
reason: 'stopOnError',
|
|
700
|
+
});
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const stepStartedAtMs = Date.now();
|
|
705
|
+
const stepArgs = {
|
|
706
|
+
...defaults,
|
|
707
|
+
...(step.arguments && typeof step.arguments === 'object' ? step.arguments : {}),
|
|
708
|
+
};
|
|
709
|
+
for (const key of ['adb', 'serial', 'port', 'packageName']) {
|
|
710
|
+
if (step[key] !== undefined) {
|
|
711
|
+
stepArgs[key] = step[key];
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const toolResult = await runner(step.command, stepArgs);
|
|
716
|
+
const parsed = parseToolResult(toolResult);
|
|
717
|
+
const passed = !parsed.isError && parsed.payload?.ok !== false;
|
|
718
|
+
const stepResult = {
|
|
719
|
+
id: step.id,
|
|
720
|
+
command: step.command,
|
|
721
|
+
status: passed ? 'passed' : 'failed',
|
|
722
|
+
ok: passed,
|
|
723
|
+
packageName: stepArgs.packageName,
|
|
724
|
+
port: stepArgs.port,
|
|
725
|
+
durationMs: Date.now() - stepStartedAtMs,
|
|
726
|
+
summary: summarizeToolPayload(parsed),
|
|
727
|
+
};
|
|
728
|
+
if (!passed) {
|
|
729
|
+
stepResult.error = parsed.payload?.error || firstTextLine(parsed.text) || 'command_failed';
|
|
730
|
+
}
|
|
731
|
+
if (includeRaw) {
|
|
732
|
+
stepResult.result = parsed.payload || undefined;
|
|
733
|
+
stepResult.rawText = parsed.payload ? undefined : truncateText(parsed.text, maxRawChars);
|
|
734
|
+
}
|
|
735
|
+
results.push(stepResult);
|
|
736
|
+
if (!passed && stopOnError) {
|
|
737
|
+
stopped = true;
|
|
738
|
+
}
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const stepResult = {
|
|
741
|
+
id: step.id,
|
|
742
|
+
command: step.command,
|
|
743
|
+
status: 'failed',
|
|
744
|
+
ok: false,
|
|
745
|
+
packageName: stepArgs.packageName,
|
|
746
|
+
port: stepArgs.port,
|
|
747
|
+
durationMs: Date.now() - stepStartedAtMs,
|
|
748
|
+
error: error.message || String(error),
|
|
749
|
+
};
|
|
750
|
+
results.push(stepResult);
|
|
751
|
+
if (stopOnError) {
|
|
752
|
+
stopped = true;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const failed = results.filter((item) => item.status === 'failed').length;
|
|
758
|
+
const skipped = results.filter((item) => item.status === 'skipped').length;
|
|
759
|
+
const passed = results.filter((item) => item.status === 'passed').length;
|
|
760
|
+
return toolJson({
|
|
761
|
+
ok: failed === 0,
|
|
762
|
+
batchId: args.batchId || generatedBatchId(),
|
|
763
|
+
mode,
|
|
764
|
+
stopOnError,
|
|
765
|
+
stepCount: normalizedSteps.length,
|
|
766
|
+
passed,
|
|
767
|
+
failed,
|
|
768
|
+
skipped,
|
|
769
|
+
durationMs: Date.now() - startedAtMs,
|
|
770
|
+
steps: results,
|
|
771
|
+
}, failed > 0);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function runBridge(command, args) {
|
|
775
|
+
return runProcess(buildBridgeCliArgs(command, args));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function parseToolResult(toolResult) {
|
|
779
|
+
const text = String(toolResult?.content?.[0]?.text || '');
|
|
780
|
+
try {
|
|
781
|
+
return {
|
|
782
|
+
isError: Boolean(toolResult?.isError),
|
|
783
|
+
text,
|
|
784
|
+
payload: JSON.parse(text),
|
|
785
|
+
};
|
|
786
|
+
} catch (_) {
|
|
787
|
+
return {
|
|
788
|
+
isError: Boolean(toolResult?.isError),
|
|
789
|
+
text,
|
|
790
|
+
payload: null,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function summarizeToolPayload(parsed) {
|
|
796
|
+
const payload = parsed.payload;
|
|
797
|
+
if (!payload || typeof payload !== 'object') {
|
|
798
|
+
return { text: truncateText(parsed.text, 500) };
|
|
799
|
+
}
|
|
800
|
+
const summary = {
|
|
801
|
+
ok: payload.ok,
|
|
802
|
+
error: payload.error || null,
|
|
803
|
+
};
|
|
804
|
+
if (payload.packageName) summary.packageName = payload.packageName;
|
|
805
|
+
if (payload.app?.packageName) summary.app = payload.app.packageName;
|
|
806
|
+
if (payload.activity) summary.activity = payload.activity;
|
|
807
|
+
if (payload.component) summary.component = payload.component;
|
|
808
|
+
if (payload.transport) summary.transport = payload.transport;
|
|
809
|
+
if (payload.source) summary.source = payload.source;
|
|
810
|
+
if (payload.path) summary.path = payload.path;
|
|
811
|
+
if (payload.debugBridge) {
|
|
812
|
+
summary.bridge = {
|
|
813
|
+
version: payload.debugBridge.version,
|
|
814
|
+
port: payload.debugBridge.port,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (payload.count !== undefined) summary.count = payload.count;
|
|
818
|
+
if (payload.nodeCount !== undefined) summary.nodeCount = payload.nodeCount;
|
|
819
|
+
if (Array.isArray(payload.items)) summary.items = payload.items.length;
|
|
820
|
+
if (payload.values && typeof payload.values === 'object') {
|
|
821
|
+
summary.values = Object.keys(payload.values).length;
|
|
822
|
+
}
|
|
823
|
+
if (payload.counts) summary.counts = payload.counts;
|
|
824
|
+
if (Array.isArray(payload.requests)) summary.requests = payload.requests.length;
|
|
825
|
+
if (Array.isArray(payload.console)) summary.console = payload.console.length;
|
|
826
|
+
if (payload.flutter?.layout?.operable) {
|
|
827
|
+
summary.flutterOperable = {
|
|
828
|
+
ok: payload.flutter.layout.operable.ok,
|
|
829
|
+
count: payload.flutter.layout.operable.count,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
if (payload.result && typeof payload.result === 'object') {
|
|
833
|
+
summary.result = {
|
|
834
|
+
ok: payload.result.ok,
|
|
835
|
+
error: payload.result.error || null,
|
|
836
|
+
value: truncateText(payload.result.value, 200),
|
|
837
|
+
bodyText: truncateText(payload.result.bodyText, 200),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return summary;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function truncateText(value, maxChars) {
|
|
844
|
+
if (value === undefined || value === null) return value;
|
|
845
|
+
const text = String(value);
|
|
846
|
+
if (text.length <= maxChars) return text;
|
|
847
|
+
return `${text.slice(0, maxChars)}...`;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function firstTextLine(value) {
|
|
851
|
+
const lines = String(value || '').split(/\r?\n/).filter((line) => line.trim());
|
|
852
|
+
return lines.find((line) => {
|
|
853
|
+
const text = line.trim().toLowerCase();
|
|
854
|
+
return text !== 'stderr:' && text !== 'stdout:';
|
|
855
|
+
}) || lines[0] || '';
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function generatedBatchId() {
|
|
859
|
+
return `batch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function buildBridgeCliArgs(command, args = {}) {
|
|
863
|
+
const cliArgs = [cliScript, command];
|
|
864
|
+
addCommonArgs(cliArgs, args);
|
|
865
|
+
addArg(cliArgs, 'initial-route', args.initialRoute);
|
|
866
|
+
addArg(cliArgs, 'activity', args.activity);
|
|
867
|
+
addArg(cliArgs, 'component', args.component);
|
|
868
|
+
addArg(cliArgs, 'action', args.action);
|
|
869
|
+
addRepeatedArg(cliArgs, 'category', args.category);
|
|
870
|
+
addArg(cliArgs, 'data', args.data);
|
|
871
|
+
addExtraArgs(cliArgs, args.extra);
|
|
872
|
+
addArg(cliArgs, 'out-file', args.outFile);
|
|
859
873
|
addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor(command, args));
|
|
860
874
|
addArg(cliArgs, 'apk-path', args.apkPath);
|
|
861
875
|
addArg(cliArgs, 'tap-x', args.tapX);
|
|
@@ -881,45 +895,45 @@ function buildBridgeCliArgs(command, args = {}) {
|
|
|
881
895
|
addArg(cliArgs, 'page-url-filter', args.pageUrlFilter);
|
|
882
896
|
addArg(cliArgs, 'url-filter', args.urlFilter);
|
|
883
897
|
addArg(cliArgs, 'method', args.method);
|
|
884
|
-
addArg(cliArgs, 'status-code', args.statusCode);
|
|
885
|
-
addArg(cliArgs, 'compact', args.compact);
|
|
886
|
-
addArg(cliArgs, 'full', args.full);
|
|
887
|
-
addArg(cliArgs, 'text-filter', args.textFilter);
|
|
888
|
-
addArg(cliArgs, 'resource-id-filter', args.resourceIdFilter);
|
|
889
|
-
addArg(cliArgs, 'class-filter', args.classFilter);
|
|
890
|
-
addArg(cliArgs, 'visible-only', args.visibleOnly);
|
|
891
|
-
addArg(cliArgs, 'max-nodes', args.maxNodes);
|
|
892
|
-
addArg(cliArgs, 'max-depth', args.maxDepth);
|
|
893
|
-
addArg(cliArgs, 'no-bodies', args.noBodies);
|
|
894
|
-
addArg(cliArgs, 'duration-ms', args.durationMs);
|
|
895
|
-
addArg(cliArgs, 'include-response-body', args.includeResponseBody);
|
|
896
|
-
addArg(cliArgs, 'body-max-bytes', args.bodyMaxBytes);
|
|
897
|
-
addArg(cliArgs, 'max-events', args.maxEvents);
|
|
898
|
-
addArg(cliArgs, 'keep-forward', args.keepForward);
|
|
899
|
-
addArg(cliArgs, 'key-code', args.keyCode);
|
|
900
|
-
addArg(cliArgs, 'payload', args.payload);
|
|
901
|
-
addArg(cliArgs, 'delta', args.delta);
|
|
902
|
-
addArg(cliArgs, 'max-swipes', args.maxSwipes);
|
|
903
|
-
addArg(cliArgs, 'permission', args.permission);
|
|
904
|
-
addArg(cliArgs, 'op', args.op);
|
|
905
|
-
addArg(cliArgs, 'mode', args.mode);
|
|
906
|
-
addArg(cliArgs, 'script', args.script);
|
|
907
|
-
addArg(cliArgs, 'selector', args.selector);
|
|
898
|
+
addArg(cliArgs, 'status-code', args.statusCode);
|
|
899
|
+
addArg(cliArgs, 'compact', args.compact);
|
|
900
|
+
addArg(cliArgs, 'full', args.full);
|
|
901
|
+
addArg(cliArgs, 'text-filter', args.textFilter);
|
|
902
|
+
addArg(cliArgs, 'resource-id-filter', args.resourceIdFilter);
|
|
903
|
+
addArg(cliArgs, 'class-filter', args.classFilter);
|
|
904
|
+
addArg(cliArgs, 'visible-only', args.visibleOnly);
|
|
905
|
+
addArg(cliArgs, 'max-nodes', args.maxNodes);
|
|
906
|
+
addArg(cliArgs, 'max-depth', args.maxDepth);
|
|
907
|
+
addArg(cliArgs, 'no-bodies', args.noBodies);
|
|
908
|
+
addArg(cliArgs, 'duration-ms', args.durationMs);
|
|
909
|
+
addArg(cliArgs, 'include-response-body', args.includeResponseBody);
|
|
910
|
+
addArg(cliArgs, 'body-max-bytes', args.bodyMaxBytes);
|
|
911
|
+
addArg(cliArgs, 'max-events', args.maxEvents);
|
|
912
|
+
addArg(cliArgs, 'keep-forward', args.keepForward);
|
|
913
|
+
addArg(cliArgs, 'key-code', args.keyCode);
|
|
914
|
+
addArg(cliArgs, 'payload', args.payload);
|
|
915
|
+
addArg(cliArgs, 'delta', args.delta);
|
|
916
|
+
addArg(cliArgs, 'max-swipes', args.maxSwipes);
|
|
917
|
+
addArg(cliArgs, 'permission', args.permission);
|
|
918
|
+
addArg(cliArgs, 'op', args.op);
|
|
919
|
+
addArg(cliArgs, 'mode', args.mode);
|
|
920
|
+
addArg(cliArgs, 'script', args.script);
|
|
921
|
+
addArg(cliArgs, 'selector', args.selector);
|
|
908
922
|
addArg(cliArgs, 'target-text', args.targetText);
|
|
909
923
|
addArg(cliArgs, 'value', args.value);
|
|
910
924
|
addArg(cliArgs, 'exact', args.exact);
|
|
911
925
|
addArg(cliArgs, 'button-text', args.buttonText);
|
|
912
926
|
addArg(cliArgs, 'resource-id', args.resourceId);
|
|
913
|
-
addArg(cliArgs, 'attempts', args.attempts);
|
|
914
|
-
addArg(cliArgs, 'interval-ms', args.intervalMs);
|
|
915
|
-
addArg(cliArgs, 'delta-x', args.deltaX);
|
|
916
|
-
addArg(cliArgs, 'delta-y', args.deltaY);
|
|
917
|
-
addArg(cliArgs, 'require-text', args.requireText);
|
|
918
|
-
addArg(cliArgs, 'absent-text', args.absentText);
|
|
919
|
-
addArg(cliArgs, 'require-activity', args.requireActivity);
|
|
920
|
-
addArg(cliArgs, 'since-id', args.sinceId);
|
|
921
|
-
addArg(cliArgs, 'since-ms', args.sinceMs);
|
|
922
|
-
addArg(cliArgs, 'limit', args.limit);
|
|
927
|
+
addArg(cliArgs, 'attempts', args.attempts);
|
|
928
|
+
addArg(cliArgs, 'interval-ms', args.intervalMs);
|
|
929
|
+
addArg(cliArgs, 'delta-x', args.deltaX);
|
|
930
|
+
addArg(cliArgs, 'delta-y', args.deltaY);
|
|
931
|
+
addArg(cliArgs, 'require-text', args.requireText);
|
|
932
|
+
addArg(cliArgs, 'absent-text', args.absentText);
|
|
933
|
+
addArg(cliArgs, 'require-activity', args.requireActivity);
|
|
934
|
+
addArg(cliArgs, 'since-id', args.sinceId);
|
|
935
|
+
addArg(cliArgs, 'since-ms', args.sinceMs);
|
|
936
|
+
addArg(cliArgs, 'limit', args.limit);
|
|
923
937
|
addArg(cliArgs, 'pid', args.pid);
|
|
924
938
|
addArg(cliArgs, 'app-pid', args.appPid);
|
|
925
939
|
addArg(cliArgs, 'tag', args.tag);
|
|
@@ -927,21 +941,21 @@ function buildBridgeCliArgs(command, args = {}) {
|
|
|
927
941
|
addArg(cliArgs, 'grep', args.grep);
|
|
928
942
|
addArg(cliArgs, 'lines', args.lines);
|
|
929
943
|
addArg(cliArgs, 'since', args.since);
|
|
930
|
-
addArg(cliArgs, 'follow', args.follow);
|
|
931
|
-
addArg(cliArgs, 'duration-sec', args.durationSec);
|
|
932
|
-
addArg(cliArgs, 'clear', args.clear);
|
|
933
|
-
addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
|
|
934
|
-
return cliArgs;
|
|
935
|
-
}
|
|
944
|
+
addArg(cliArgs, 'follow', args.follow);
|
|
945
|
+
addArg(cliArgs, 'duration-sec', args.durationSec);
|
|
946
|
+
addArg(cliArgs, 'clear', args.clear);
|
|
947
|
+
addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
|
|
948
|
+
return cliArgs;
|
|
949
|
+
}
|
|
936
950
|
|
|
937
951
|
async function runSmoke(args) {
|
|
938
952
|
const cliArgs = [cliScript, 'smoke'];
|
|
939
|
-
addCommonArgs(cliArgs, args);
|
|
940
|
-
addArg(cliArgs, 'out-file', args.outFile);
|
|
941
|
-
addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor('smoke', args));
|
|
942
|
-
addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
|
|
943
|
-
return runProcess(cliArgs);
|
|
944
|
-
}
|
|
953
|
+
addCommonArgs(cliArgs, args);
|
|
954
|
+
addArg(cliArgs, 'out-file', args.outFile);
|
|
955
|
+
addArg(cliArgs, 'artifact-dir', args.artifactDir || defaultArtifactDirFor('smoke', args));
|
|
956
|
+
addArg(cliArgs, 'skip-flutter-launch', args.skipFlutterLaunch);
|
|
957
|
+
return runProcess(cliArgs);
|
|
958
|
+
}
|
|
945
959
|
|
|
946
960
|
function defaultArtifactDirFor(command, args) {
|
|
947
961
|
if (args.outFile) return '';
|
|
@@ -956,35 +970,35 @@ function addCommonArgs(cliArgs, args) {
|
|
|
956
970
|
addArg(cliArgs, 'package-name', args.packageName);
|
|
957
971
|
}
|
|
958
972
|
|
|
959
|
-
function addArg(cliArgs, name, value) {
|
|
960
|
-
if (value === undefined || value === null || value === '' || value === false) {
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
cliArgs.push(`--${name}`, String(value));
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
function addRepeatedArg(cliArgs, name, value) {
|
|
967
|
-
if (Array.isArray(value)) {
|
|
968
|
-
for (const item of value) addArg(cliArgs, name, item);
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
addArg(cliArgs, name, value);
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
function addExtraArgs(cliArgs, value) {
|
|
975
|
-
if (Array.isArray(value)) {
|
|
976
|
-
for (const item of value) addArg(cliArgs, 'extra', item);
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
if (value && typeof value === 'object') {
|
|
980
|
-
for (const [key, extraValue] of Object.entries(value)) {
|
|
981
|
-
addArg(cliArgs, 'extra', `${key}=${extraValue}`);
|
|
982
|
-
}
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
addArg(cliArgs, 'extra', value);
|
|
986
|
-
}
|
|
987
|
-
|
|
973
|
+
function addArg(cliArgs, name, value) {
|
|
974
|
+
if (value === undefined || value === null || value === '' || value === false) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
cliArgs.push(`--${name}`, String(value));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function addRepeatedArg(cliArgs, name, value) {
|
|
981
|
+
if (Array.isArray(value)) {
|
|
982
|
+
for (const item of value) addArg(cliArgs, name, item);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
addArg(cliArgs, name, value);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function addExtraArgs(cliArgs, value) {
|
|
989
|
+
if (Array.isArray(value)) {
|
|
990
|
+
for (const item of value) addArg(cliArgs, 'extra', item);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
if (value && typeof value === 'object') {
|
|
994
|
+
for (const [key, extraValue] of Object.entries(value)) {
|
|
995
|
+
addArg(cliArgs, 'extra', `${key}=${extraValue}`);
|
|
996
|
+
}
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
addArg(cliArgs, 'extra', value);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
988
1002
|
function runProcess(cliArgs) {
|
|
989
1003
|
return new Promise((resolve) => {
|
|
990
1004
|
const child = spawn(nodeBinary, cliArgs, {
|
|
@@ -1009,23 +1023,23 @@ function runProcess(cliArgs) {
|
|
|
1009
1023
|
code === 0 ? '' : `exitCode: ${code}`,
|
|
1010
1024
|
retryWithPackageNameHint(cliArgs, stdout, stderr, code),
|
|
1011
1025
|
].filter(Boolean).join('\n\n');
|
|
1012
|
-
resolve(toolText(text || emptyProcessText(cliArgs), code !== 0));
|
|
1013
|
-
});
|
|
1014
|
-
});
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function emptyProcessText(cliArgs) {
|
|
1018
|
-
const command = cliArgs[1] || '';
|
|
1019
|
-
if (command === 'logcat' && cliArgs.includes('--app-pid')) {
|
|
1020
|
-
return 'logcat: no matching lines for current app pid';
|
|
1021
|
-
}
|
|
1022
|
-
if (command === 'logcat') {
|
|
1023
|
-
return 'logcat: no matching lines';
|
|
1024
|
-
}
|
|
1025
|
-
return 'ok';
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
|
|
1026
|
+
resolve(toolText(text || emptyProcessText(cliArgs), code !== 0));
|
|
1027
|
+
});
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function emptyProcessText(cliArgs) {
|
|
1032
|
+
const command = cliArgs[1] || '';
|
|
1033
|
+
if (command === 'logcat' && cliArgs.includes('--app-pid')) {
|
|
1034
|
+
return 'logcat: no matching lines for current app pid';
|
|
1035
|
+
}
|
|
1036
|
+
if (command === 'logcat') {
|
|
1037
|
+
return 'logcat: no matching lines';
|
|
1038
|
+
}
|
|
1039
|
+
return 'ok';
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
|
|
1029
1043
|
if (code === 0 || cliArgs.includes('--package-name') || cliArgs.includes('--port')) {
|
|
1030
1044
|
return '';
|
|
1031
1045
|
}
|
|
@@ -1036,25 +1050,25 @@ function retryWithPackageNameHint(cliArgs, stdout, stderr, code) {
|
|
|
1036
1050
|
return 'agentHint: Default bridge port 18080 accepted the connection but did not answer. If you know the target app, retry this tool with packageName so the CLI can discover that app bridge port.';
|
|
1037
1051
|
}
|
|
1038
1052
|
|
|
1039
|
-
function toolText(text, isError = false) {
|
|
1040
|
-
return {
|
|
1041
|
-
content: [
|
|
1042
|
-
{
|
|
1043
|
-
type: 'text',
|
|
1053
|
+
function toolText(text, isError = false) {
|
|
1054
|
+
return {
|
|
1055
|
+
content: [
|
|
1056
|
+
{
|
|
1057
|
+
type: 'text',
|
|
1044
1058
|
text,
|
|
1045
1059
|
},
|
|
1046
1060
|
],
|
|
1047
|
-
isError,
|
|
1048
|
-
};
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
function toolJson(value, isError = false) {
|
|
1052
|
-
return toolText(JSON.stringify(value, null, 2), isError);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function sendResult(id, result) {
|
|
1056
|
-
send({ jsonrpc: '2.0', id, result });
|
|
1057
|
-
}
|
|
1061
|
+
isError,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function toolJson(value, isError = false) {
|
|
1066
|
+
return toolText(JSON.stringify(value, null, 2), isError);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function sendResult(id, result) {
|
|
1070
|
+
send({ jsonrpc: '2.0', id, result });
|
|
1071
|
+
}
|
|
1058
1072
|
|
|
1059
1073
|
function sendError(id, code, message) {
|
|
1060
1074
|
send({
|
|
@@ -1064,27 +1078,27 @@ function sendError(id, code, message) {
|
|
|
1064
1078
|
});
|
|
1065
1079
|
}
|
|
1066
1080
|
|
|
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
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function writeLog(text) {
|
|
1078
|
-
process.stderr.write(`${text}\n`);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
if (require.main === module) {
|
|
1082
|
-
startServer();
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
module.exports = {
|
|
1086
|
-
buildBridgeCliArgs,
|
|
1087
|
-
readNextMessage,
|
|
1088
|
-
runBatch,
|
|
1089
|
-
startServer,
|
|
1090
|
-
};
|
|
1081
|
+
function send(message) {
|
|
1082
|
+
const body = Buffer.from(JSON.stringify(message), 'utf8');
|
|
1083
|
+
if (responseFormat === 'line') {
|
|
1084
|
+
process.stdout.write(`${body.toString('utf8')}\n`);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
1088
|
+
process.stdout.write(body);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function writeLog(text) {
|
|
1092
|
+
process.stderr.write(`${text}\n`);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (require.main === module) {
|
|
1096
|
+
startServer();
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
module.exports = {
|
|
1100
|
+
buildBridgeCliArgs,
|
|
1101
|
+
readNextMessage,
|
|
1102
|
+
runBatch,
|
|
1103
|
+
startServer,
|
|
1104
|
+
};
|