@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.
Files changed (89) hide show
  1. package/dist/android-adapter.d.ts.map +1 -0
  2. package/dist/android-adapter.js +89 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +12 -0
  5. package/dist/tools/accessibility.d.ts.map +1 -0
  6. package/dist/tools/accessibility.js +85 -0
  7. package/dist/tools/adb.d.ts.map +1 -0
  8. package/dist/tools/adb.js +66 -0
  9. package/dist/tools/app-state.d.ts.map +1 -0
  10. package/dist/tools/app-state.js +173 -0
  11. package/dist/tools/diagnose.d.ts.map +1 -0
  12. package/dist/tools/diagnose.js +128 -0
  13. package/dist/tools/hot-reload.d.ts.map +1 -0
  14. package/dist/tools/hot-reload.js +97 -0
  15. package/dist/tools/index.d.ts.map +1 -0
  16. package/dist/tools/index.js +66 -0
  17. package/dist/tools/interaction.d.ts.map +1 -0
  18. package/dist/tools/interaction.js +395 -0
  19. package/dist/tools/logcat.d.ts.map +1 -0
  20. package/dist/tools/logcat.js +216 -0
  21. package/dist/tools/network.d.ts.map +1 -0
  22. package/dist/tools/network.js +123 -0
  23. package/dist/tools/performance.d.ts.map +1 -0
  24. package/dist/tools/performance.js +143 -0
  25. package/dist/tools/recording.d.ts.map +1 -0
  26. package/dist/tools/recording.js +102 -0
  27. package/dist/tools/rn-tools.d.ts.map +1 -0
  28. package/dist/tools/rn-tools.js +120 -0
  29. package/dist/tools/smart-actions.d.ts.map +1 -0
  30. package/dist/tools/smart-actions.js +506 -0
  31. package/dist/tools/ui-tree.d.ts.map +1 -0
  32. package/dist/tools/ui-tree.js +226 -0
  33. package/dist/transport/adb-client.d.ts.map +1 -0
  34. package/dist/transport/adb-client.js +124 -0
  35. package/dist/transport/adb-client.test.d.ts.map +1 -0
  36. package/dist/transport/adb-client.test.js +153 -0
  37. package/dist/transport/agent-client.d.ts.map +1 -0
  38. package/dist/transport/agent-client.js +157 -0
  39. package/dist/transport/agent-client.test.d.ts.map +1 -0
  40. package/dist/transport/agent-client.test.js +199 -0
  41. package/dist/transport/connection-manager.d.ts.map +1 -0
  42. package/dist/transport/connection-manager.js +119 -0
  43. package/dist/util/logcat-parser.d.ts.map +1 -0
  44. package/dist/util/logcat-parser.js +79 -0
  45. package/dist/util/safety.d.ts.map +1 -0
  46. package/dist/util/safety.js +132 -0
  47. package/dist/util/safety.test.d.ts.map +1 -0
  48. package/dist/util/safety.test.js +205 -0
  49. package/dist/util/text-extractor.d.ts.map +1 -0
  50. package/dist/util/text-extractor.js +71 -0
  51. package/dist/util/ui-tree-cache.d.ts.map +1 -0
  52. package/dist/util/ui-tree-cache.js +46 -0
  53. package/dist/util/ui-tree-cache.test.d.ts.map +1 -0
  54. package/dist/util/ui-tree-cache.test.js +84 -0
  55. package/dist/util/ui-tree-parser.d.ts.map +1 -0
  56. package/dist/util/ui-tree-parser.js +123 -0
  57. package/dist/util/ui-tree-parser.test.d.ts.map +1 -0
  58. package/dist/util/ui-tree-parser.test.js +167 -0
  59. package/package.json +22 -0
  60. package/src/android-adapter.ts +124 -0
  61. package/src/index.ts +8 -0
  62. package/src/tools/accessibility.ts +94 -0
  63. package/src/tools/adb.ts +75 -0
  64. package/src/tools/app-state.ts +193 -0
  65. package/src/tools/diagnose.ts +146 -0
  66. package/src/tools/hot-reload.ts +103 -0
  67. package/src/tools/index.ts +66 -0
  68. package/src/tools/interaction.ts +448 -0
  69. package/src/tools/logcat.ts +252 -0
  70. package/src/tools/network.ts +145 -0
  71. package/src/tools/performance.ts +169 -0
  72. package/src/tools/recording.ts +123 -0
  73. package/src/tools/rn-tools.ts +143 -0
  74. package/src/tools/smart-actions.ts +593 -0
  75. package/src/tools/ui-tree.ts +258 -0
  76. package/src/transport/adb-client.test.ts +228 -0
  77. package/src/transport/adb-client.ts +139 -0
  78. package/src/transport/agent-client.test.ts +267 -0
  79. package/src/transport/agent-client.ts +188 -0
  80. package/src/transport/connection-manager.ts +140 -0
  81. package/src/util/logcat-parser.ts +94 -0
  82. package/src/util/safety.test.ts +251 -0
  83. package/src/util/safety.ts +143 -0
  84. package/src/util/text-extractor.ts +87 -0
  85. package/src/util/ui-tree-cache.test.ts +105 -0
  86. package/src/util/ui-tree-cache.ts +54 -0
  87. package/src/util/ui-tree-parser.test.ts +182 -0
  88. package/src/util/ui-tree-parser.ts +169 -0
  89. package/tsconfig.json +11 -0
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * React Native Tools - Inspect RN component tree and bridge activity.
4
+ *
5
+ * These tools require the on-device agent (Phase 2) for full functionality.
6
+ * In Phase 1, they provide guidance on what data will be available once
7
+ * the agent is installed.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.registerRnTools = registerRnTools;
11
+ const zod_1 = require("zod");
12
+ /** Message returned when the agent is required but not connected. */
13
+ const AGENT_REQUIRED_MSG = (tool) => JSON.stringify({
14
+ error: "agent_not_connected",
15
+ tool,
16
+ message: "This tool requires the EmuDebug on-device agent (Phase 2). " +
17
+ "The agent provides deep React Native introspection by hooking into " +
18
+ "the Hermes JS engine and React fiber tree. " +
19
+ "Use ADB-based tools (get_ui_tree, get_logcat) for Phase 1 debugging.",
20
+ alternatives: [
21
+ "get_ui_tree - View hierarchy via uiautomator",
22
+ "get_screen_text - All visible text on screen",
23
+ "get_logcat - Filter logs by ReactNativeJS tag",
24
+ "get_crash_info - JS exceptions from logcat",
25
+ ],
26
+ });
27
+ function registerRnTools(server, cm) {
28
+ /**
29
+ * get_rn_component_tree - Get the React Native component tree via Hermes CDP.
30
+ */
31
+ server.tool("get_rn_component_tree", "Get the React Native component tree from the Hermes engine. Shows component names, props, state, and hooks. Requires on-device agent (Phase 2); falls back to guidance in Phase 1.", {
32
+ componentFilter: zod_1.z.string().optional().describe("Filter by component name (substring match)"),
33
+ maxDepth: zod_1.z.number().optional().describe("Maximum tree depth to return"),
34
+ includeProps: zod_1.z.boolean().optional().describe("Include component props (default: true)"),
35
+ includeState: zod_1.z.boolean().optional().describe("Include component state (default: true)"),
36
+ }, async (params) => {
37
+ if (!cm.agent.isConnected()) {
38
+ return {
39
+ content: [{ type: "text", text: AGENT_REQUIRED_MSG("get_rn_component_tree") }],
40
+ };
41
+ }
42
+ try {
43
+ const result = await cm.agent.call("getRnComponentTree", {
44
+ componentFilter: params.componentFilter,
45
+ maxDepth: params.maxDepth ?? 0,
46
+ includeProps: params.includeProps ?? true,
47
+ includeState: params.includeState ?? true,
48
+ });
49
+ return {
50
+ content: [{ type: "text", text: JSON.stringify(result) }],
51
+ };
52
+ }
53
+ catch (err) {
54
+ return {
55
+ content: [
56
+ {
57
+ type: "text",
58
+ text: `Error getting RN component tree: ${err instanceof Error ? err.message : String(err)}`,
59
+ },
60
+ ],
61
+ isError: true,
62
+ };
63
+ }
64
+ });
65
+ /**
66
+ * get_rn_bridge - Inspect React Native bridge / TurboModule activity.
67
+ */
68
+ server.tool("get_rn_bridge", "Inspect React Native TurboModule calls and bridge traffic. Shows recent native module invocations and their parameters. Requires on-device agent (Phase 2).", {
69
+ moduleName: zod_1.z.string().optional().describe("Filter by native module name"),
70
+ maxEntries: zod_1.z.number().optional().describe("Maximum entries to return (default: 50)"),
71
+ includeArgs: zod_1.z.boolean().optional().describe("Include call arguments (default: true)"),
72
+ }, async (params) => {
73
+ if (!cm.agent.isConnected()) {
74
+ // In Phase 1, we can at least check logcat for bridge-related messages
75
+ try {
76
+ const bridgeLogs = await cm.adb.shell("logcat -d -v threadtime | grep -i 'turbo\\|bridge\\|nativemodule' | tail -30");
77
+ return {
78
+ content: [
79
+ {
80
+ type: "text",
81
+ text: JSON.stringify({
82
+ phase1Mode: true,
83
+ message: "Full bridge inspection requires the on-device agent (Phase 2). " +
84
+ "Showing bridge-related logcat entries as a fallback.",
85
+ logEntries: bridgeLogs.trim().split("\n").filter(Boolean),
86
+ }, null, 2),
87
+ },
88
+ ],
89
+ };
90
+ }
91
+ catch {
92
+ return {
93
+ content: [{ type: "text", text: AGENT_REQUIRED_MSG("get_rn_bridge") }],
94
+ };
95
+ }
96
+ }
97
+ try {
98
+ const result = await cm.agent.call("getRnBridge", {
99
+ moduleName: params.moduleName,
100
+ maxEntries: params.maxEntries ?? 50,
101
+ includeArgs: params.includeArgs ?? true,
102
+ });
103
+ return {
104
+ content: [{ type: "text", text: JSON.stringify(result) }],
105
+ };
106
+ }
107
+ catch (err) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: `Error getting bridge data: ${err instanceof Error ? err.message : String(err)}`,
113
+ },
114
+ ],
115
+ isError: true,
116
+ };
117
+ }
118
+ });
119
+ }
120
+ //# sourceMappingURL=rn-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smart-actions.d.ts","sourceRoot":"","sources":["../../src/tools/smart-actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAqC5E,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CA4hBvF"}
@@ -0,0 +1,506 @@
1
+ "use strict";
2
+ /**
3
+ * Smart Actions - Higher-level compound tools that reduce round-trips.
4
+ *
5
+ * Provides:
6
+ * - wait_for_text: Poll until text appears on screen (navigation waits)
7
+ * - scroll_to_text: Scroll until an element with matching text is visible
8
+ * - fill_form: Batch-fill multiple form fields in one call
9
+ * - tap_and_wait: Tap element then wait for expected text
10
+ * - assert_screen: Quick screen assertions (contains / notContains)
11
+ * - open_deep_link: Open app via deep link URL scheme
12
+ * - take_screenshot: Capture screen as base64 PNG image
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.registerSmartActionTools = registerSmartActionTools;
16
+ const zod_1 = require("zod");
17
+ const ui_tree_parser_js_1 = require("../util/ui-tree-parser.js");
18
+ const text_extractor_js_1 = require("../util/text-extractor.js");
19
+ const ui_tree_js_1 = require("./ui-tree.js");
20
+ const promises_1 = require("node:fs/promises");
21
+ const node_os_1 = require("node:os");
22
+ const node_path_1 = require("node:path");
23
+ /** Sleep helper */
24
+ function sleep(ms) {
25
+ return new Promise((resolve) => setTimeout(resolve, ms));
26
+ }
27
+ /**
28
+ * Find an element by text/resourceId/contentDescription in the tree.
29
+ */
30
+ function findElement(flat, selector) {
31
+ for (const node of flat) {
32
+ if (selector.text && node.text.toLowerCase().includes(selector.text.toLowerCase())) {
33
+ return node;
34
+ }
35
+ if (selector.resourceId && node.resourceId.includes(selector.resourceId)) {
36
+ return node;
37
+ }
38
+ if (selector.contentDescription &&
39
+ node.contentDescription.toLowerCase().includes(selector.contentDescription.toLowerCase())) {
40
+ return node;
41
+ }
42
+ }
43
+ return null;
44
+ }
45
+ function registerSmartActionTools(server, cm) {
46
+ /**
47
+ * wait_for_text - Poll until specific text appears on screen.
48
+ */
49
+ server.tool("wait_for_text", "Wait until specific text appears on the screen. Polls the UI tree at intervals until the text is found or timeout is reached. Essential after navigation, screen transitions, or async operations.", {
50
+ text: zod_1.z.string().describe("Text to wait for (case-insensitive substring match)"),
51
+ timeoutMs: zod_1.z.number().optional().describe("Maximum wait time in ms (default: 10000)"),
52
+ pollIntervalMs: zod_1.z.number().optional().describe("Polling interval in ms (default: 1000)"),
53
+ }, async (params) => {
54
+ const timeout = params.timeoutMs ?? 10_000;
55
+ const interval = params.pollIntervalMs ?? 1_000;
56
+ const startTime = Date.now();
57
+ const searchLower = params.text.toLowerCase();
58
+ let attempts = 0;
59
+ while (Date.now() - startTime < timeout) {
60
+ attempts++;
61
+ // Force fresh dump each poll
62
+ cm.uiCache.invalidate();
63
+ try {
64
+ const tree = await (0, ui_tree_js_1.getCachedTree)(cm, { visibleOnly: true, includeSystemUI: false });
65
+ const texts = (0, text_extractor_js_1.extractTextStrings)(tree);
66
+ const found = texts.some((t) => t.toLowerCase().includes(searchLower));
67
+ if (found) {
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: JSON.stringify({
73
+ success: true,
74
+ found: true,
75
+ text: params.text,
76
+ elapsedMs: Date.now() - startTime,
77
+ attempts,
78
+ }),
79
+ },
80
+ ],
81
+ };
82
+ }
83
+ }
84
+ catch {
85
+ // UI dump failed, retry
86
+ }
87
+ if (Date.now() - startTime + interval < timeout) {
88
+ await sleep(interval);
89
+ }
90
+ }
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: JSON.stringify({
96
+ success: false,
97
+ found: false,
98
+ text: params.text,
99
+ elapsedMs: Date.now() - startTime,
100
+ attempts,
101
+ hint: "Text did not appear within timeout. Check if the expected screen transition occurred.",
102
+ }),
103
+ },
104
+ ],
105
+ };
106
+ });
107
+ /**
108
+ * scroll_to_text - Scroll until element with text is visible.
109
+ */
110
+ server.tool("scroll_to_text", "Scroll the screen until an element containing the specified text becomes visible. Useful for finding elements in long scrollable lists or forms.", {
111
+ text: zod_1.z.string().describe("Text to scroll to (case-insensitive substring match)"),
112
+ direction: zod_1.z.enum(["up", "down"]).optional().describe("Scroll direction (default: down)"),
113
+ maxScrolls: zod_1.z.number().optional().describe("Maximum scroll attempts (default: 10)"),
114
+ }, async (params) => {
115
+ const direction = params.direction ?? "down";
116
+ const maxScrolls = params.maxScrolls ?? 10;
117
+ const searchLower = params.text.toLowerCase();
118
+ // Screen dimensions for scroll coordinates (assume 1080x2340 default, adjust from first tree)
119
+ let screenWidth = 1080;
120
+ let screenHeight = 2340;
121
+ for (let i = 0; i < maxScrolls; i++) {
122
+ cm.uiCache.invalidate();
123
+ try {
124
+ const tree = await (0, ui_tree_js_1.getCachedTree)(cm, { visibleOnly: true, includeSystemUI: false });
125
+ const flat = (0, ui_tree_parser_js_1.flattenTree)(tree);
126
+ // Detect screen size from root node bounds
127
+ if (flat.length > 0 && flat[0].bounds) {
128
+ screenWidth = flat[0].bounds.right;
129
+ screenHeight = flat[0].bounds.bottom;
130
+ }
131
+ // Check if target text is visible
132
+ const match = flat.find((n) => n.text.toLowerCase().includes(searchLower) ||
133
+ n.contentDescription.toLowerCase().includes(searchLower));
134
+ if (match && match.bounds) {
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: JSON.stringify({
140
+ success: true,
141
+ found: true,
142
+ text: match.text || match.contentDescription,
143
+ center: {
144
+ x: Math.round((match.bounds.left + match.bounds.right) / 2),
145
+ y: Math.round((match.bounds.top + match.bounds.bottom) / 2),
146
+ },
147
+ scrollsNeeded: i,
148
+ }),
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ }
154
+ catch {
155
+ // Continue scrolling
156
+ }
157
+ // Perform scroll
158
+ const centerX = Math.round(screenWidth / 2);
159
+ const startY = direction === "down"
160
+ ? Math.round(screenHeight * 0.7)
161
+ : Math.round(screenHeight * 0.3);
162
+ const endY = direction === "down"
163
+ ? Math.round(screenHeight * 0.3)
164
+ : Math.round(screenHeight * 0.7);
165
+ await cm.adb.shell(`input swipe ${centerX} ${startY} ${centerX} ${endY} 300`);
166
+ // Brief pause for scroll animation to settle
167
+ await sleep(500);
168
+ }
169
+ return {
170
+ content: [
171
+ {
172
+ type: "text",
173
+ text: JSON.stringify({
174
+ success: false,
175
+ found: false,
176
+ text: params.text,
177
+ scrollAttempts: maxScrolls,
178
+ hint: `Text "${params.text}" not found after ${maxScrolls} scroll attempts.`,
179
+ }),
180
+ },
181
+ ],
182
+ };
183
+ });
184
+ /**
185
+ * fill_form - Fill multiple form fields in one call.
186
+ */
187
+ server.tool("fill_form", "Fill multiple form fields in a single call. Each field is identified by text, resourceId, or contentDescription. Taps the field, clears it, types the value. Much more efficient than separate tap+type calls for each field.", {
188
+ fields: zod_1.z
189
+ .array(zod_1.z.object({
190
+ selector: zod_1.z.object({
191
+ text: zod_1.z.string().optional().describe("Find field by visible text/label"),
192
+ resourceId: zod_1.z.string().optional().describe("Find field by resource ID"),
193
+ contentDescription: zod_1.z.string().optional().describe("Find field by content description"),
194
+ }).describe("How to find the field"),
195
+ value: zod_1.z.string().describe("Text to enter in the field"),
196
+ clearFirst: zod_1.z.boolean().optional().describe("Clear existing text before typing (default: true)"),
197
+ }))
198
+ .describe("Array of field selectors and values to fill"),
199
+ }, async (params) => {
200
+ const results = [];
201
+ for (const field of params.fields) {
202
+ const clearFirst = field.clearFirst ?? true;
203
+ const fieldId = field.selector.text || field.selector.resourceId || field.selector.contentDescription || "unknown";
204
+ try {
205
+ // Force fresh tree for each field (screen changes after typing)
206
+ cm.uiCache.invalidate();
207
+ const tree = await (0, ui_tree_js_1.getCachedTree)(cm, { visibleOnly: true, includeSystemUI: false });
208
+ const flat = (0, ui_tree_parser_js_1.flattenTree)(tree);
209
+ const node = findElement(flat, field.selector);
210
+ if (!node || !node.bounds) {
211
+ results.push({ field: fieldId, success: false, error: "Element not found" });
212
+ continue;
213
+ }
214
+ const x = Math.round((node.bounds.left + node.bounds.right) / 2);
215
+ const y = Math.round((node.bounds.top + node.bounds.bottom) / 2);
216
+ // Tap the field
217
+ await cm.adb.shell(`input tap ${x} ${y}`);
218
+ await sleep(300); // Wait for field to focus
219
+ // Clear if needed
220
+ if (clearFirst) {
221
+ await cm.adb.shell("input keyevent 123"); // MOVE_END
222
+ await cm.adb.shell("input keyevent " + Array(50).fill("67").join(" "));
223
+ }
224
+ // Type the value
225
+ const escaped = field.value
226
+ .replace(/\\/g, "\\\\")
227
+ .replace(/ /g, "%s")
228
+ .replace(/'/g, "\\'")
229
+ .replace(/"/g, '\\"')
230
+ .replace(/&/g, "\\&")
231
+ .replace(/</g, "\\<")
232
+ .replace(/>/g, "\\>")
233
+ .replace(/\|/g, "\\|")
234
+ .replace(/;/g, "\\;")
235
+ .replace(/\(/g, "\\(")
236
+ .replace(/\)/g, "\\)")
237
+ .replace(/\$/g, "\\$")
238
+ .replace(/`/g, "\\`");
239
+ await cm.adb.shell(`input text ${escaped}`);
240
+ // Dismiss keyboard with ESCAPE to prevent focus issues with next field
241
+ await cm.adb.shell("input keyevent 111"); // ESCAPE
242
+ results.push({ field: fieldId, success: true });
243
+ }
244
+ catch (err) {
245
+ results.push({
246
+ field: fieldId,
247
+ success: false,
248
+ error: err instanceof Error ? err.message : String(err),
249
+ });
250
+ }
251
+ }
252
+ const allSuccess = results.every((r) => r.success);
253
+ return {
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: JSON.stringify({
258
+ success: allSuccess,
259
+ filledCount: results.filter((r) => r.success).length,
260
+ totalFields: params.fields.length,
261
+ results,
262
+ }),
263
+ },
264
+ ],
265
+ };
266
+ });
267
+ /**
268
+ * tap_and_wait - Tap an element then wait for expected text to appear.
269
+ */
270
+ server.tool("tap_and_wait", "Tap an element and wait for expected text to appear on screen. Combines tap + wait_for_text in a single call to reduce round-trips.", {
271
+ text: zod_1.z.string().optional().describe("Visible text to tap"),
272
+ resourceId: zod_1.z.string().optional().describe("Android resource ID to tap"),
273
+ contentDescription: zod_1.z.string().optional().describe("Content description to tap"),
274
+ x: zod_1.z.number().optional().describe("X coordinate to tap (fallback)"),
275
+ y: zod_1.z.number().optional().describe("Y coordinate to tap (fallback)"),
276
+ waitForText: zod_1.z.string().describe("Text to wait for after tap"),
277
+ timeoutMs: zod_1.z.number().optional().describe("Max wait time in ms (default: 10000)"),
278
+ }, async (params) => {
279
+ try {
280
+ // Step 1: Find and tap the element
281
+ cm.uiCache.invalidate();
282
+ let tapX;
283
+ let tapY;
284
+ let matchedBy = "";
285
+ if (params.text || params.resourceId || params.contentDescription) {
286
+ const tree = await (0, ui_tree_js_1.getCachedTree)(cm, { visibleOnly: true, includeSystemUI: false });
287
+ const flat = (0, ui_tree_parser_js_1.flattenTree)(tree);
288
+ const match = findElement(flat, {
289
+ text: params.text,
290
+ resourceId: params.resourceId,
291
+ contentDescription: params.contentDescription,
292
+ });
293
+ if (match?.bounds) {
294
+ tapX = Math.round((match.bounds.left + match.bounds.right) / 2);
295
+ tapY = Math.round((match.bounds.top + match.bounds.bottom) / 2);
296
+ matchedBy = params.text
297
+ ? `text="${match.text}"`
298
+ : params.resourceId
299
+ ? `resourceId="${match.resourceId}"`
300
+ : `contentDescription="${match.contentDescription}"`;
301
+ }
302
+ }
303
+ if (tapX === undefined || tapY === undefined) {
304
+ if (params.x !== undefined && params.y !== undefined) {
305
+ tapX = params.x;
306
+ tapY = params.y;
307
+ matchedBy = `coordinates (${tapX}, ${tapY})`;
308
+ }
309
+ else {
310
+ return {
311
+ content: [
312
+ {
313
+ type: "text",
314
+ text: JSON.stringify({
315
+ success: false,
316
+ error: "Element not found",
317
+ hint: "Use get_screen_text to see available elements.",
318
+ }),
319
+ },
320
+ ],
321
+ };
322
+ }
323
+ }
324
+ cm.uiCache.invalidate();
325
+ await cm.adb.shell(`input tap ${tapX} ${tapY}`);
326
+ // Step 2: Wait for text to appear
327
+ const timeout = params.timeoutMs ?? 10_000;
328
+ const interval = 500;
329
+ const startTime = Date.now();
330
+ const searchLower = params.waitForText.toLowerCase();
331
+ while (Date.now() - startTime < timeout) {
332
+ await sleep(interval);
333
+ cm.uiCache.invalidate();
334
+ try {
335
+ const tree = await (0, ui_tree_js_1.getCachedTree)(cm, { visibleOnly: true, includeSystemUI: false });
336
+ const texts = (0, text_extractor_js_1.extractTextStrings)(tree);
337
+ const found = texts.some((t) => t.toLowerCase().includes(searchLower));
338
+ if (found) {
339
+ return {
340
+ content: [
341
+ {
342
+ type: "text",
343
+ text: JSON.stringify({
344
+ success: true,
345
+ tappedBy: matchedBy,
346
+ tapped: { x: tapX, y: tapY },
347
+ waitedFor: params.waitForText,
348
+ elapsedMs: Date.now() - startTime,
349
+ }),
350
+ },
351
+ ],
352
+ };
353
+ }
354
+ }
355
+ catch {
356
+ // UI dump failed, retry
357
+ }
358
+ }
359
+ return {
360
+ content: [
361
+ {
362
+ type: "text",
363
+ text: JSON.stringify({
364
+ success: false,
365
+ tappedBy: matchedBy,
366
+ tapped: { x: tapX, y: tapY },
367
+ waitedFor: params.waitForText,
368
+ timedOut: true,
369
+ elapsedMs: Date.now() - startTime,
370
+ hint: "Tap succeeded but expected text did not appear within timeout.",
371
+ }),
372
+ },
373
+ ],
374
+ };
375
+ }
376
+ catch (err) {
377
+ return {
378
+ content: [
379
+ {
380
+ type: "text",
381
+ text: `tap_and_wait failed: ${err instanceof Error ? err.message : String(err)}`,
382
+ },
383
+ ],
384
+ isError: true,
385
+ };
386
+ }
387
+ });
388
+ /**
389
+ * assert_screen - Quick screen assertions without returning the full tree.
390
+ */
391
+ server.tool("assert_screen", "Quickly verify screen contains (or doesn't contain) expected text. Returns pass/fail without the full UI tree, saving tokens.", {
392
+ contains: zod_1.z.array(zod_1.z.string()).optional().describe("Text strings that MUST be on screen"),
393
+ notContains: zod_1.z.array(zod_1.z.string()).optional().describe("Text strings that must NOT be on screen"),
394
+ }, async (params) => {
395
+ try {
396
+ cm.uiCache.invalidate();
397
+ const tree = await (0, ui_tree_js_1.getCachedTree)(cm, { visibleOnly: true, includeSystemUI: false });
398
+ const allText = (0, text_extractor_js_1.extractTextStrings)(tree).map((t) => t.toLowerCase());
399
+ const missing = [];
400
+ const unexpected = [];
401
+ for (const expected of params.contains ?? []) {
402
+ if (!allText.some((t) => t.includes(expected.toLowerCase()))) {
403
+ missing.push(expected);
404
+ }
405
+ }
406
+ for (const banned of params.notContains ?? []) {
407
+ if (allText.some((t) => t.includes(banned.toLowerCase()))) {
408
+ unexpected.push(banned);
409
+ }
410
+ }
411
+ const pass = missing.length === 0 && unexpected.length === 0;
412
+ return {
413
+ content: [
414
+ {
415
+ type: "text",
416
+ text: JSON.stringify({
417
+ pass,
418
+ ...(missing.length > 0 ? { missing } : {}),
419
+ ...(unexpected.length > 0 ? { unexpected } : {}),
420
+ }),
421
+ },
422
+ ],
423
+ };
424
+ }
425
+ catch (err) {
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: `assert_screen failed: ${err instanceof Error ? err.message : String(err)}`,
431
+ },
432
+ ],
433
+ isError: true,
434
+ };
435
+ }
436
+ });
437
+ /**
438
+ * open_deep_link - Open app via deep link URL scheme.
439
+ */
440
+ server.tool("open_deep_link", "Open the app via a deep link URL scheme. Navigates directly to a specific screen without tapping through the UI.", {
441
+ url: zod_1.z.string().describe("Deep link URL (e.g., myapp://screen/detail?id=123 or https://example.com/path)"),
442
+ }, async (params) => {
443
+ try {
444
+ cm.uiCache.invalidate();
445
+ await cm.adb.shell(`am start -a android.intent.action.VIEW -d "${params.url}"`);
446
+ return {
447
+ content: [
448
+ {
449
+ type: "text",
450
+ text: JSON.stringify({ success: true, url: params.url }),
451
+ },
452
+ ],
453
+ };
454
+ }
455
+ catch (err) {
456
+ return {
457
+ content: [
458
+ {
459
+ type: "text",
460
+ text: `open_deep_link failed: ${err instanceof Error ? err.message : String(err)}`,
461
+ },
462
+ ],
463
+ isError: true,
464
+ };
465
+ }
466
+ });
467
+ /**
468
+ * take_screenshot - Capture the screen as a base64 PNG image.
469
+ */
470
+ server.tool("take_screenshot", "Capture a screenshot of the current screen and return it as a base64-encoded PNG image. Useful for visual verification of the app state.", {}, async () => {
471
+ try {
472
+ const localPath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `emudebug-screenshot-${Date.now()}.png`);
473
+ // Capture screenshot on device and pull to local
474
+ await cm.adb.shell("screencap -p /sdcard/emudebug_screenshot.png");
475
+ await cm.adb.exec(["pull", "/sdcard/emudebug_screenshot.png", localPath]);
476
+ // Clean up device file (fire and forget)
477
+ cm.adb.shell("rm -f /sdcard/emudebug_screenshot.png").catch(() => { });
478
+ // Read the local file as base64
479
+ const imageBuffer = await (0, promises_1.readFile)(localPath);
480
+ const base64 = imageBuffer.toString("base64");
481
+ // Clean up local file
482
+ await (0, promises_1.unlink)(localPath).catch(() => { });
483
+ return {
484
+ content: [
485
+ {
486
+ type: "image",
487
+ data: base64,
488
+ mimeType: "image/png",
489
+ },
490
+ ],
491
+ };
492
+ }
493
+ catch (err) {
494
+ return {
495
+ content: [
496
+ {
497
+ type: "text",
498
+ text: `Error taking screenshot: ${err instanceof Error ? err.message : String(err)}`,
499
+ },
500
+ ],
501
+ isError: true,
502
+ };
503
+ }
504
+ });
505
+ }
506
+ //# sourceMappingURL=smart-actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ui-tree.d.ts","sourceRoot":"","sources":["../../src/tools/ui-tree.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAA4B,KAAK,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAsBlF;;;GAGG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,iBAAiB,EACrB,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE,GACvG,OAAO,CAAC,MAAM,EAAE,CAAC,CA4BnB;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,iBAAiB,GAAG,IAAI,CA0LlF"}