@sensaiorg/adapter-android 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/android-adapter.d.ts.map +1 -0
- package/dist/android-adapter.js +89 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/tools/accessibility.d.ts.map +1 -0
- package/dist/tools/accessibility.js +85 -0
- package/dist/tools/adb.d.ts.map +1 -0
- package/dist/tools/adb.js +66 -0
- package/dist/tools/app-state.d.ts.map +1 -0
- package/dist/tools/app-state.js +173 -0
- package/dist/tools/diagnose.d.ts.map +1 -0
- package/dist/tools/diagnose.js +128 -0
- package/dist/tools/hot-reload.d.ts.map +1 -0
- package/dist/tools/hot-reload.js +97 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/interaction.d.ts.map +1 -0
- package/dist/tools/interaction.js +395 -0
- package/dist/tools/logcat.d.ts.map +1 -0
- package/dist/tools/logcat.js +216 -0
- package/dist/tools/network.d.ts.map +1 -0
- package/dist/tools/network.js +123 -0
- package/dist/tools/performance.d.ts.map +1 -0
- package/dist/tools/performance.js +143 -0
- package/dist/tools/recording.d.ts.map +1 -0
- package/dist/tools/recording.js +102 -0
- package/dist/tools/rn-tools.d.ts.map +1 -0
- package/dist/tools/rn-tools.js +120 -0
- package/dist/tools/smart-actions.d.ts.map +1 -0
- package/dist/tools/smart-actions.js +506 -0
- package/dist/tools/ui-tree.d.ts.map +1 -0
- package/dist/tools/ui-tree.js +226 -0
- package/dist/transport/adb-client.d.ts.map +1 -0
- package/dist/transport/adb-client.js +124 -0
- package/dist/transport/adb-client.test.d.ts.map +1 -0
- package/dist/transport/adb-client.test.js +153 -0
- package/dist/transport/agent-client.d.ts.map +1 -0
- package/dist/transport/agent-client.js +157 -0
- package/dist/transport/agent-client.test.d.ts.map +1 -0
- package/dist/transport/agent-client.test.js +199 -0
- package/dist/transport/connection-manager.d.ts.map +1 -0
- package/dist/transport/connection-manager.js +119 -0
- package/dist/util/logcat-parser.d.ts.map +1 -0
- package/dist/util/logcat-parser.js +79 -0
- package/dist/util/safety.d.ts.map +1 -0
- package/dist/util/safety.js +132 -0
- package/dist/util/safety.test.d.ts.map +1 -0
- package/dist/util/safety.test.js +205 -0
- package/dist/util/text-extractor.d.ts.map +1 -0
- package/dist/util/text-extractor.js +71 -0
- package/dist/util/ui-tree-cache.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.js +46 -0
- package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
- package/dist/util/ui-tree-cache.test.js +84 -0
- package/dist/util/ui-tree-parser.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.js +123 -0
- package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
- package/dist/util/ui-tree-parser.test.js +167 -0
- package/package.json +22 -0
- package/src/android-adapter.ts +124 -0
- package/src/index.ts +8 -0
- package/src/tools/accessibility.ts +94 -0
- package/src/tools/adb.ts +75 -0
- package/src/tools/app-state.ts +193 -0
- package/src/tools/diagnose.ts +146 -0
- package/src/tools/hot-reload.ts +103 -0
- package/src/tools/index.ts +66 -0
- package/src/tools/interaction.ts +448 -0
- package/src/tools/logcat.ts +252 -0
- package/src/tools/network.ts +145 -0
- package/src/tools/performance.ts +169 -0
- package/src/tools/recording.ts +123 -0
- package/src/tools/rn-tools.ts +143 -0
- package/src/tools/smart-actions.ts +593 -0
- package/src/tools/ui-tree.ts +258 -0
- package/src/transport/adb-client.test.ts +228 -0
- package/src/transport/adb-client.ts +139 -0
- package/src/transport/agent-client.test.ts +267 -0
- package/src/transport/agent-client.ts +188 -0
- package/src/transport/connection-manager.ts +140 -0
- package/src/util/logcat-parser.ts +94 -0
- package/src/util/safety.test.ts +251 -0
- package/src/util/safety.ts +143 -0
- package/src/util/text-extractor.ts +87 -0
- package/src/util/ui-tree-cache.test.ts +105 -0
- package/src/util/ui-tree-cache.ts +54 -0
- package/src/util/ui-tree-parser.test.ts +182 -0
- package/src/util/ui-tree-parser.ts +169 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Hot Reload Tool - Trigger Metro bundler hot or full reload.
|
|
4
|
+
*
|
|
5
|
+
* Communicates with the Metro dev server to trigger a reload of the
|
|
6
|
+
* JavaScript bundle, either incremental (hot) or full.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.registerHotReloadTools = registerHotReloadTools;
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
/** Default Metro bundler port. */
|
|
12
|
+
const METRO_PORT = 8081;
|
|
13
|
+
function registerHotReloadTools(server, cm) {
|
|
14
|
+
server.tool("hot_reload", "Trigger a React Native reload via the Metro bundler. 'hot' performs a fast refresh (preserves state), 'full' performs a complete bundle reload. Works by sending key events or hitting the Metro dev server endpoint.", {
|
|
15
|
+
type: zod_1.z
|
|
16
|
+
.enum(["hot", "full"])
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Reload type: 'hot' for fast refresh (default), 'full' for complete reload"),
|
|
19
|
+
}, async (params) => {
|
|
20
|
+
const reloadType = params.type ?? "hot";
|
|
21
|
+
try {
|
|
22
|
+
if (reloadType === "full") {
|
|
23
|
+
// Method 1: Send "RR" key sequence (double-tap R in dev menu)
|
|
24
|
+
// This triggers a full reload in React Native dev mode
|
|
25
|
+
try {
|
|
26
|
+
await cm.adb.shell("input keyevent 46 46"); // R R
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Method 2: Hit Metro /reload endpoint
|
|
30
|
+
try {
|
|
31
|
+
await cm.adb.shell(`curl -s http://localhost:${METRO_PORT}/reload`);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Method 3: Send the menu key then look for reload option
|
|
35
|
+
await cm.adb.shell("input keyevent 82"); // MENU key opens RN dev menu
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: JSON.stringify({
|
|
43
|
+
success: true,
|
|
44
|
+
type: "full",
|
|
45
|
+
message: "Full reload triggered. The app will reload the JS bundle from Metro.",
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Hot reload: trigger via Metro /message endpoint
|
|
53
|
+
try {
|
|
54
|
+
// Send HMR trigger through ADB reverse + Metro
|
|
55
|
+
await cm.adb.shell(`curl -s -X POST http://localhost:${METRO_PORT}/message -H "Content-Type: application/json" -d '{"method":"reload"}'`);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Fallback: the shake gesture or menu key
|
|
59
|
+
// On emulators, Ctrl+M or MENU key opens dev menu
|
|
60
|
+
await cm.adb.shell("input keyevent 82");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: JSON.stringify({
|
|
67
|
+
success: true,
|
|
68
|
+
type: "hot",
|
|
69
|
+
message: "Hot reload triggered. Fast Refresh will apply changes without losing state.",
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: JSON.stringify({
|
|
82
|
+
success: false,
|
|
83
|
+
error: err instanceof Error ? err.message : String(err),
|
|
84
|
+
hints: [
|
|
85
|
+
"Ensure Metro bundler is running (npx react-native start)",
|
|
86
|
+
"Ensure ADB reverse port forwarding is set up: adb reverse tcp:8081 tcp:8081",
|
|
87
|
+
"Ensure the app is in dev mode (not a release build)",
|
|
88
|
+
],
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=hot-reload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAe5E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CAc/E"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool Registry - Registers all EmuDebug MCP tools with the server.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerAllTools = registerAllTools;
|
|
7
|
+
const ui_tree_js_1 = require("./ui-tree.js");
|
|
8
|
+
const accessibility_js_1 = require("./accessibility.js");
|
|
9
|
+
const logcat_js_1 = require("./logcat.js");
|
|
10
|
+
const rn_tools_js_1 = require("./rn-tools.js");
|
|
11
|
+
const app_state_js_1 = require("./app-state.js");
|
|
12
|
+
const network_js_1 = require("./network.js");
|
|
13
|
+
const interaction_js_1 = require("./interaction.js");
|
|
14
|
+
const performance_js_1 = require("./performance.js");
|
|
15
|
+
const adb_js_1 = require("./adb.js");
|
|
16
|
+
const diagnose_js_1 = require("./diagnose.js");
|
|
17
|
+
const hot_reload_js_1 = require("./hot-reload.js");
|
|
18
|
+
const smart_actions_js_1 = require("./smart-actions.js");
|
|
19
|
+
const recording_js_1 = require("./recording.js");
|
|
20
|
+
/**
|
|
21
|
+
* Register all 26 EmuDebug tools with the MCP server.
|
|
22
|
+
*
|
|
23
|
+
* Tools registered:
|
|
24
|
+
* 1. get_ui_tree - UI hierarchy as JSON
|
|
25
|
+
* 2. get_screen_text - Visible text extraction
|
|
26
|
+
* 3. get_element_details - Deep element inspection
|
|
27
|
+
* 4. get_accessibility - Accessibility tree
|
|
28
|
+
* 5. get_logcat - Filtered Android logs
|
|
29
|
+
* 6. get_crash_info - JS exceptions, native crashes, ANRs
|
|
30
|
+
* 7. get_rn_component_tree - React Native component tree (Phase 2)
|
|
31
|
+
* 8. get_rn_bridge - TurboModule / bridge inspection (Phase 2)
|
|
32
|
+
* 9. get_app_state - AsyncStorage, query cache, nav, auth, etc.
|
|
33
|
+
* 10. get_network - HTTP traffic inspection
|
|
34
|
+
* 11. tap - Tap by text/resourceId/coordinates
|
|
35
|
+
* 12. type_text - Type into focused field
|
|
36
|
+
* 13. swipe - Swipe gesture
|
|
37
|
+
* 14. press_key - Android key events
|
|
38
|
+
* 15. long_press - Long press (press and hold)
|
|
39
|
+
* 16. get_performance - FPS, memory, CPU, bridge metrics
|
|
40
|
+
* 17. run_adb - Safe ADB command passthrough
|
|
41
|
+
* 18. diagnose_screen - Composite screen diagnosis (parallelized)
|
|
42
|
+
* 19. hot_reload - Metro bundler hot/full reload
|
|
43
|
+
* 20. wait_for_text - Poll until text appears on screen
|
|
44
|
+
* 21. scroll_to_text - Scroll until element is visible
|
|
45
|
+
* 22. fill_form - Batch fill multiple form fields
|
|
46
|
+
* 23. tap_and_wait - Tap element then wait for text
|
|
47
|
+
* 24. assert_screen - Quick screen assertions
|
|
48
|
+
* 25. open_deep_link - Open app via deep link URL
|
|
49
|
+
* 26. take_screenshot - Capture screen as base64 PNG
|
|
50
|
+
*/
|
|
51
|
+
function registerAllTools(server, cm) {
|
|
52
|
+
(0, ui_tree_js_1.registerUiTreeTools)(server, cm); // 3 tools: get_ui_tree, get_screen_text, get_element_details
|
|
53
|
+
(0, accessibility_js_1.registerAccessibilityTools)(server, cm); // 1 tool: get_accessibility
|
|
54
|
+
(0, logcat_js_1.registerLogcatTools)(server, cm); // 2 tools: get_logcat, get_crash_info
|
|
55
|
+
(0, rn_tools_js_1.registerRnTools)(server, cm); // 2 tools: get_rn_component_tree, get_rn_bridge
|
|
56
|
+
(0, app_state_js_1.registerAppStateTools)(server, cm); // 1 tool: get_app_state
|
|
57
|
+
(0, network_js_1.registerNetworkTools)(server, cm); // 1 tool: get_network
|
|
58
|
+
(0, interaction_js_1.registerInteractionTools)(server, cm); // 5 tools: tap, type_text, swipe, press_key, long_press
|
|
59
|
+
(0, performance_js_1.registerPerformanceTools)(server, cm); // 1 tool: get_performance
|
|
60
|
+
(0, adb_js_1.registerAdbTools)(server, cm); // 1 tool: run_adb
|
|
61
|
+
(0, diagnose_js_1.registerDiagnoseTools)(server, cm); // 1 tool: diagnose_screen
|
|
62
|
+
(0, hot_reload_js_1.registerHotReloadTools)(server, cm); // 1 tool: hot_reload
|
|
63
|
+
(0, smart_actions_js_1.registerSmartActionTools)(server, cm); // 7 tools: wait_for_text, scroll_to_text, fill_form, tap_and_wait, assert_screen, open_deep_link, take_screenshot
|
|
64
|
+
(0, recording_js_1.registerRecordingTools)(server, cm); // 2 tools: start_recording, stop_recording
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction.d.ts","sourceRoot":"","sources":["../../src/tools/interaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAmE5E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CA+WvF"}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Interaction Tools - Simulate user input on the device.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - tap: Tap by text, resourceId, contentDescription, or coordinates
|
|
7
|
+
* - type_text: Enter text into the focused field
|
|
8
|
+
* - swipe: Swipe gesture between two points
|
|
9
|
+
* - press_key: Press Android key events (BACK, HOME, ENTER, etc.)
|
|
10
|
+
* - long_press: Long press (press and hold) on element or coordinates
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.registerInteractionTools = registerInteractionTools;
|
|
14
|
+
const zod_1 = require("zod");
|
|
15
|
+
const ui_tree_parser_js_1 = require("../util/ui-tree-parser.js");
|
|
16
|
+
const ui_tree_js_1 = require("./ui-tree.js");
|
|
17
|
+
/** Android key event codes for common keys. */
|
|
18
|
+
const KEY_CODES = {
|
|
19
|
+
BACK: 4,
|
|
20
|
+
HOME: 3,
|
|
21
|
+
ENTER: 66,
|
|
22
|
+
DELETE: 67,
|
|
23
|
+
TAB: 61,
|
|
24
|
+
ESCAPE: 111,
|
|
25
|
+
VOLUME_UP: 24,
|
|
26
|
+
VOLUME_DOWN: 25,
|
|
27
|
+
POWER: 26,
|
|
28
|
+
MENU: 82,
|
|
29
|
+
SEARCH: 84,
|
|
30
|
+
DPAD_UP: 19,
|
|
31
|
+
DPAD_DOWN: 20,
|
|
32
|
+
DPAD_LEFT: 21,
|
|
33
|
+
DPAD_RIGHT: 22,
|
|
34
|
+
DPAD_CENTER: 23,
|
|
35
|
+
APP_SWITCH: 187,
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Find element coordinates by searching the UI tree (uses cache).
|
|
39
|
+
*/
|
|
40
|
+
async function findElementCenter(cm, selector) {
|
|
41
|
+
const tree = await (0, ui_tree_js_1.getCachedTree)(cm, { visibleOnly: true, includeSystemUI: false });
|
|
42
|
+
const flat = (0, ui_tree_parser_js_1.flattenTree)(tree);
|
|
43
|
+
for (const node of flat) {
|
|
44
|
+
let matched = false;
|
|
45
|
+
let matchedBy = "";
|
|
46
|
+
if (selector.text && node.text.toLowerCase().includes(selector.text.toLowerCase())) {
|
|
47
|
+
matched = true;
|
|
48
|
+
matchedBy = `text="${node.text}"`;
|
|
49
|
+
}
|
|
50
|
+
if (selector.resourceId && node.resourceId.includes(selector.resourceId)) {
|
|
51
|
+
matched = true;
|
|
52
|
+
matchedBy = `resourceId="${node.resourceId}"`;
|
|
53
|
+
}
|
|
54
|
+
if (selector.contentDescription &&
|
|
55
|
+
node.contentDescription.toLowerCase().includes(selector.contentDescription.toLowerCase())) {
|
|
56
|
+
matched = true;
|
|
57
|
+
matchedBy = `contentDescription="${node.contentDescription}"`;
|
|
58
|
+
}
|
|
59
|
+
if (matched && node.bounds) {
|
|
60
|
+
return {
|
|
61
|
+
x: Math.round((node.bounds.left + node.bounds.right) / 2),
|
|
62
|
+
y: Math.round((node.bounds.top + node.bounds.bottom) / 2),
|
|
63
|
+
matched: matchedBy,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function registerInteractionTools(server, cm) {
|
|
70
|
+
/**
|
|
71
|
+
* tap - Tap on an element or coordinates.
|
|
72
|
+
*/
|
|
73
|
+
server.tool("tap", "Tap on a UI element found by text, resourceId, or contentDescription; or tap directly at x,y coordinates. The element is found in the current UI hierarchy and tapped at its center.", {
|
|
74
|
+
text: zod_1.z.string().optional().describe("Find and tap element containing this text"),
|
|
75
|
+
resourceId: zod_1.z.string().optional().describe("Find and tap element with this resource ID"),
|
|
76
|
+
contentDescription: zod_1.z.string().optional().describe("Find and tap element with this content description"),
|
|
77
|
+
x: zod_1.z.number().optional().describe("X coordinate for direct tap"),
|
|
78
|
+
y: zod_1.z.number().optional().describe("Y coordinate for direct tap"),
|
|
79
|
+
}, async (params) => {
|
|
80
|
+
try {
|
|
81
|
+
let tapX;
|
|
82
|
+
let tapY;
|
|
83
|
+
let matchInfo = "";
|
|
84
|
+
if (params.x !== undefined && params.y !== undefined) {
|
|
85
|
+
// Direct coordinate tap
|
|
86
|
+
tapX = params.x;
|
|
87
|
+
tapY = params.y;
|
|
88
|
+
matchInfo = `direct coordinates (${tapX}, ${tapY})`;
|
|
89
|
+
}
|
|
90
|
+
else if (params.text || params.resourceId || params.contentDescription) {
|
|
91
|
+
// Find element by selector
|
|
92
|
+
const result = await findElementCenter(cm, {
|
|
93
|
+
text: params.text,
|
|
94
|
+
resourceId: params.resourceId,
|
|
95
|
+
contentDescription: params.contentDescription,
|
|
96
|
+
});
|
|
97
|
+
if (!result) {
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text",
|
|
102
|
+
text: JSON.stringify({
|
|
103
|
+
success: false,
|
|
104
|
+
error: "Element not found",
|
|
105
|
+
selector: {
|
|
106
|
+
text: params.text,
|
|
107
|
+
resourceId: params.resourceId,
|
|
108
|
+
contentDescription: params.contentDescription,
|
|
109
|
+
},
|
|
110
|
+
hint: "Use get_screen_text or get_ui_tree to see available elements.",
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
tapX = result.x;
|
|
117
|
+
tapY = result.y;
|
|
118
|
+
matchInfo = result.matched;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: "text",
|
|
125
|
+
text: JSON.stringify({
|
|
126
|
+
success: false,
|
|
127
|
+
error: "No target specified. Provide text, resourceId, contentDescription, or x/y coordinates.",
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Invalidate cache — screen will change after tap
|
|
134
|
+
cm.uiCache.invalidate();
|
|
135
|
+
await cm.adb.shell(`input tap ${tapX} ${tapY}`);
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{
|
|
139
|
+
type: "text",
|
|
140
|
+
text: JSON.stringify({
|
|
141
|
+
success: true,
|
|
142
|
+
tapped: { x: tapX, y: tapY },
|
|
143
|
+
matchedBy: matchInfo,
|
|
144
|
+
}),
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return {
|
|
151
|
+
content: [
|
|
152
|
+
{
|
|
153
|
+
type: "text",
|
|
154
|
+
text: `Error performing tap: ${err instanceof Error ? err.message : String(err)}`,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
isError: true,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
/**
|
|
162
|
+
* type_text - Type text into the currently focused input field.
|
|
163
|
+
*/
|
|
164
|
+
server.tool("type_text", "Type text into the currently focused input field on the device. Special characters are escaped for ADB input.", {
|
|
165
|
+
text: zod_1.z.string().describe("Text to type"),
|
|
166
|
+
clearFirst: zod_1.z.boolean().optional().describe("Clear the field before typing (default: false)"),
|
|
167
|
+
}, async (params) => {
|
|
168
|
+
try {
|
|
169
|
+
// Optionally clear existing text
|
|
170
|
+
if (params.clearFirst) {
|
|
171
|
+
// Most reliable approach for React Native TextInput:
|
|
172
|
+
// Move to end, then send batch DEL key events to clear everything.
|
|
173
|
+
// Ctrl+A doesn't work reliably in RN, and shift+home is fragile.
|
|
174
|
+
await cm.adb.shell("input keyevent 123"); // KEYCODE_MOVE_END
|
|
175
|
+
// Send 50 DEL events in one call (enough for any reasonable field)
|
|
176
|
+
await cm.adb.shell("input keyevent " + Array(50).fill("67").join(" "));
|
|
177
|
+
}
|
|
178
|
+
// Escape special characters for ADB input text
|
|
179
|
+
const escaped = params.text
|
|
180
|
+
.replace(/\\/g, "\\\\")
|
|
181
|
+
.replace(/ /g, "%s")
|
|
182
|
+
.replace(/'/g, "\\'")
|
|
183
|
+
.replace(/"/g, '\\"')
|
|
184
|
+
.replace(/&/g, "\\&")
|
|
185
|
+
.replace(/</g, "\\<")
|
|
186
|
+
.replace(/>/g, "\\>")
|
|
187
|
+
.replace(/\|/g, "\\|")
|
|
188
|
+
.replace(/;/g, "\\;")
|
|
189
|
+
.replace(/\(/g, "\\(")
|
|
190
|
+
.replace(/\)/g, "\\)")
|
|
191
|
+
.replace(/\$/g, "\\$")
|
|
192
|
+
.replace(/`/g, "\\`");
|
|
193
|
+
// Invalidate cache — screen content changes after typing
|
|
194
|
+
cm.uiCache.invalidate();
|
|
195
|
+
await cm.adb.shell(`input text ${escaped}`);
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "text",
|
|
200
|
+
text: JSON.stringify({ ok: true }),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
return {
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: `Error typing text: ${err instanceof Error ? err.message : String(err)}`,
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
isError: true,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
/**
|
|
218
|
+
* swipe - Perform a swipe gesture.
|
|
219
|
+
*/
|
|
220
|
+
server.tool("swipe", "Perform a swipe gesture on the device from (startX, startY) to (endX, endY) over the specified duration.", {
|
|
221
|
+
startX: zod_1.z.number().describe("Start X coordinate"),
|
|
222
|
+
startY: zod_1.z.number().describe("Start Y coordinate"),
|
|
223
|
+
endX: zod_1.z.number().describe("End X coordinate"),
|
|
224
|
+
endY: zod_1.z.number().describe("End Y coordinate"),
|
|
225
|
+
durationMs: zod_1.z.number().optional().describe("Swipe duration in milliseconds (default: 300)"),
|
|
226
|
+
}, async (params) => {
|
|
227
|
+
try {
|
|
228
|
+
const duration = params.durationMs ?? 300;
|
|
229
|
+
// Invalidate cache — screen will change after swipe
|
|
230
|
+
cm.uiCache.invalidate();
|
|
231
|
+
await cm.adb.shell(`input swipe ${params.startX} ${params.startY} ${params.endX} ${params.endY} ${duration}`);
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{
|
|
235
|
+
type: "text",
|
|
236
|
+
text: JSON.stringify({ ok: true }),
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `Error performing swipe: ${err instanceof Error ? err.message : String(err)}`,
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
isError: true,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
/**
|
|
254
|
+
* press_key - Press an Android key event.
|
|
255
|
+
*/
|
|
256
|
+
server.tool("press_key", "Press an Android key event. Supports named keys (BACK, HOME, ENTER, etc.) or numeric key codes.", {
|
|
257
|
+
key: zod_1.z
|
|
258
|
+
.string()
|
|
259
|
+
.describe(`Key name or numeric code. Named keys: ${Object.keys(KEY_CODES).join(", ")}`),
|
|
260
|
+
}, async (params) => {
|
|
261
|
+
try {
|
|
262
|
+
const keyUpper = params.key.toUpperCase();
|
|
263
|
+
const keyCode = KEY_CODES[keyUpper] ?? parseInt(params.key, 10);
|
|
264
|
+
if (isNaN(keyCode)) {
|
|
265
|
+
return {
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: JSON.stringify({
|
|
270
|
+
success: false,
|
|
271
|
+
error: `Unknown key: ${params.key}`,
|
|
272
|
+
availableKeys: Object.keys(KEY_CODES),
|
|
273
|
+
}),
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
// Invalidate cache — key press may change screen
|
|
279
|
+
cm.uiCache.invalidate();
|
|
280
|
+
await cm.adb.shell(`input keyevent ${keyCode}`);
|
|
281
|
+
return {
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: JSON.stringify({ ok: true }),
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
return {
|
|
292
|
+
content: [
|
|
293
|
+
{
|
|
294
|
+
type: "text",
|
|
295
|
+
text: `Error pressing key: ${err instanceof Error ? err.message : String(err)}`,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
isError: true,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
/**
|
|
303
|
+
* long_press - Long press (press and hold) on an element or coordinates.
|
|
304
|
+
*/
|
|
305
|
+
server.tool("long_press", "Long press (press and hold) at coordinates or on an element found by text. Useful for context menus and drag initiation.", {
|
|
306
|
+
text: zod_1.z.string().optional().describe("Visible text to long-press"),
|
|
307
|
+
resourceId: zod_1.z.string().optional().describe("Resource ID to long-press"),
|
|
308
|
+
contentDescription: zod_1.z.string().optional().describe("Content description to long-press"),
|
|
309
|
+
x: zod_1.z.number().optional().describe("X coordinate"),
|
|
310
|
+
y: zod_1.z.number().optional().describe("Y coordinate"),
|
|
311
|
+
durationMs: zod_1.z.number().optional().describe("Hold duration in ms (default: 1500)"),
|
|
312
|
+
}, async (params) => {
|
|
313
|
+
try {
|
|
314
|
+
let pressX;
|
|
315
|
+
let pressY;
|
|
316
|
+
let matchInfo = "";
|
|
317
|
+
if (params.x !== undefined && params.y !== undefined) {
|
|
318
|
+
pressX = params.x;
|
|
319
|
+
pressY = params.y;
|
|
320
|
+
matchInfo = `direct coordinates (${pressX}, ${pressY})`;
|
|
321
|
+
}
|
|
322
|
+
else if (params.text || params.resourceId || params.contentDescription) {
|
|
323
|
+
const result = await findElementCenter(cm, {
|
|
324
|
+
text: params.text,
|
|
325
|
+
resourceId: params.resourceId,
|
|
326
|
+
contentDescription: params.contentDescription,
|
|
327
|
+
});
|
|
328
|
+
if (!result) {
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: "text",
|
|
333
|
+
text: JSON.stringify({
|
|
334
|
+
success: false,
|
|
335
|
+
error: "Element not found",
|
|
336
|
+
selector: {
|
|
337
|
+
text: params.text,
|
|
338
|
+
resourceId: params.resourceId,
|
|
339
|
+
contentDescription: params.contentDescription,
|
|
340
|
+
},
|
|
341
|
+
hint: "Use get_screen_text or get_ui_tree to see available elements.",
|
|
342
|
+
}),
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
pressX = result.x;
|
|
348
|
+
pressY = result.y;
|
|
349
|
+
matchInfo = result.matched;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
return {
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: "text",
|
|
356
|
+
text: JSON.stringify({
|
|
357
|
+
success: false,
|
|
358
|
+
error: "No target specified. Provide text, resourceId, contentDescription, or x/y coordinates.",
|
|
359
|
+
}),
|
|
360
|
+
},
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const duration = params.durationMs ?? 1500;
|
|
365
|
+
// A zero-distance swipe = long press
|
|
366
|
+
cm.uiCache.invalidate();
|
|
367
|
+
await cm.adb.shell(`input swipe ${pressX} ${pressY} ${pressX} ${pressY} ${duration}`);
|
|
368
|
+
return {
|
|
369
|
+
content: [
|
|
370
|
+
{
|
|
371
|
+
type: "text",
|
|
372
|
+
text: JSON.stringify({
|
|
373
|
+
success: true,
|
|
374
|
+
longPressed: { x: pressX, y: pressY },
|
|
375
|
+
durationMs: duration,
|
|
376
|
+
matchedBy: matchInfo,
|
|
377
|
+
}),
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
return {
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: `Error performing long press: ${err instanceof Error ? err.message : String(err)}`,
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
isError: true,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
//# sourceMappingURL=interaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logcat.d.ts","sourceRoot":"","sources":["../../src/tools/logcat.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AA4B5E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CAqNlF"}
|