@srmorete/mobile-device-mcp 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 (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/bin/cli.js +2 -0
  4. package/drivers/android/app-debug-androidTest.apk +0 -0
  5. package/drivers/android/app-debug.apk +0 -0
  6. package/drivers/ios/Debug-iphonesimulator/FlyingFox.o +0 -0
  7. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
  8. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
  9. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
  10. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  11. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
  12. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
  13. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  14. package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
  15. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.o +0 -0
  16. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
  17. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
  18. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
  19. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  20. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
  21. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
  22. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  23. package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
  24. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/Info.plist +0 -0
  25. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/PkgInfo +1 -0
  26. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/UITreeServerApp +0 -0
  27. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/UITreeServerApp.debug.dylib +0 -0
  28. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/_CodeSignature/CodeResources +128 -0
  29. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/__preview.dylib +0 -0
  30. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
  31. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
  32. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
  33. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  34. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
  35. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
  36. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  37. package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
  38. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/Info.plist +0 -0
  39. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/Testing +0 -0
  40. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/_CodeSignature/CodeResources +168 -0
  41. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/version.plist +18 -0
  42. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/Info.plist +0 -0
  43. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/XCTAutomationSupport +0 -0
  44. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/_CodeSignature/CodeResources +113 -0
  45. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/version.plist +18 -0
  46. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/Info.plist +0 -0
  47. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/XCTest +0 -0
  48. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/_CodeSignature/CodeResources +817 -0
  49. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/version.plist +18 -0
  50. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/Info.plist +0 -0
  51. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/XCTestCore +0 -0
  52. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/_CodeSignature/CodeResources +113 -0
  53. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/version.plist +18 -0
  54. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/Info.plist +0 -0
  55. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/XCTestSupport +0 -0
  56. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/_CodeSignature/CodeResources +113 -0
  57. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/version.plist +18 -0
  58. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/Info.plist +0 -0
  59. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/XCUIAutomation +0 -0
  60. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/_CodeSignature/CodeResources +432 -0
  61. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/version.plist +18 -0
  62. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/Info.plist +0 -0
  63. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/XCUnit +0 -0
  64. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/_CodeSignature/CodeResources +113 -0
  65. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/version.plist +18 -0
  66. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/libXCTestSwiftSupport.dylib +0 -0
  67. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Info.plist +254 -0
  68. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PkgInfo +1 -0
  69. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/Info.plist +0 -0
  70. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/UITreeServerUITests +0 -0
  71. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/_CodeSignature/CodeResources +101 -0
  72. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/UITreeServerUITests-Runner +0 -0
  73. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/_CodeSignature/CodeResources +458 -0
  74. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
  75. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
  76. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
  77. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  78. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
  79. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
  80. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  81. package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
  82. package/drivers/ios/UITreeServerUITests_iphonesimulator26.2-arm64-x86_64.xctestrun +135 -0
  83. package/drivers/ios/UITreeServerUITests_iphonesimulator26.2-arm64.xctestrun +135 -0
  84. package/package.json +32 -0
  85. package/src/filter/filter.ts +393 -0
  86. package/src/filter/index.ts +42 -0
  87. package/src/filter/types.ts +70 -0
  88. package/src/server/bootstrap.ts +367 -0
  89. package/src/server/devices.ts +262 -0
  90. package/src/server/index.ts +41 -0
  91. package/src/server/ports.ts +190 -0
  92. package/src/server/proxy.ts +119 -0
  93. package/src/server/tools.ts +303 -0
  94. package/src/server/types.ts +22 -0
@@ -0,0 +1,135 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>UITreeServerUITests</key>
6
+ <dict>
7
+ <key>BlueprintName</key>
8
+ <string>UITreeServerUITests</string>
9
+ <key>BlueprintProviderName</key>
10
+ <string>UITreeServer</string>
11
+ <key>BlueprintProviderRelativePath</key>
12
+ <string>UITreeServer.xcodeproj</string>
13
+ <key>BundleIdentifiersForCrashReportEmphasis</key>
14
+ <array>
15
+ <string>dev.uitreeserver.app</string>
16
+ <string>dev.uitreeserver.uitests</string>
17
+ </array>
18
+ <key>CommandLineArguments</key>
19
+ <array/>
20
+ <key>DefaultTestExecutionTimeAllowance</key>
21
+ <integer>600</integer>
22
+ <key>DependentProductPaths</key>
23
+ <array>
24
+ <string>__TESTROOT__/Debug-iphonesimulator/UITreeServerApp.app</string>
25
+ <string>__TESTROOT__/Debug-iphonesimulator/UITreeServerUITests-Runner.app</string>
26
+ <string>__TESTROOT__/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest</string>
27
+ </array>
28
+ <key>DiagnosticCollectionPolicy</key>
29
+ <integer>1</integer>
30
+ <key>EnvironmentVariables</key>
31
+ <dict>
32
+ <key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
33
+ <string>com.apple.AppStore</string>
34
+ <key>DYLD_INSERT_LIBRARIES</key>
35
+ <string>/usr/lib/libRPAC.dylib</string>
36
+ <key>OS_ACTIVITY_DT_MODE</key>
37
+ <string>YES</string>
38
+ <key>PERFC_ENABLE_EXTENDED_DIAGNOSTIC_FORMAT</key>
39
+ <string>1</string>
40
+ <key>PERFC_ENABLE_PROFILE_MODE</key>
41
+ <string>1</string>
42
+ <key>PERFC_RESET_INSERT_LIBRARIES</key>
43
+ <string>1</string>
44
+ <key>PERFC_SUPPRESS_SYSTEM_REPORTS</key>
45
+ <string>1</string>
46
+ <key>SQLITE_ENABLE_THREAD_ASSERTIONS</key>
47
+ <string>1</string>
48
+ <key>TERM</key>
49
+ <string>dumb</string>
50
+ </dict>
51
+ <key>IsUITestBundle</key>
52
+ <true/>
53
+ <key>IsXCTRunnerHostedTestBundle</key>
54
+ <true/>
55
+ <key>PreferredScreenCaptureFormat</key>
56
+ <string>screenRecording</string>
57
+ <key>ProductModuleName</key>
58
+ <string>UITreeServerUITests</string>
59
+ <key>RunOrder</key>
60
+ <integer>0</integer>
61
+ <key>SystemAttachmentLifetime</key>
62
+ <string>deleteOnSuccess</string>
63
+ <key>TestBundlePath</key>
64
+ <string>__TESTHOST__/PlugIns/UITreeServerUITests.xctest</string>
65
+ <key>TestHostBundleIdentifier</key>
66
+ <string>dev.uitreeserver.uitests.xctrunner</string>
67
+ <key>TestHostPath</key>
68
+ <string>__TESTROOT__/Debug-iphonesimulator/UITreeServerUITests-Runner.app</string>
69
+ <key>TestLanguage</key>
70
+ <string></string>
71
+ <key>TestRegion</key>
72
+ <string></string>
73
+ <key>TestTimeoutsEnabled</key>
74
+ <false/>
75
+ <key>TestingEnvironmentVariables</key>
76
+ <dict>
77
+ <key>DYLD_FRAMEWORK_PATH</key>
78
+ <string>__TESTROOT__/Debug-iphonesimulator:__TESTROOT__/Debug-iphonesimulator/PackageFrameworks:__PLATFORMS__/iPhoneSimulator.platform/Developer/Library/Frameworks</string>
79
+ <key>DYLD_INSERT_LIBRARIES</key>
80
+ <string>__SIMRUNTIMEROOT__/usr/lib/libMainThreadChecker.dylib:/usr/lib/libRPAC.dylib</string>
81
+ <key>DYLD_LIBRARY_PATH</key>
82
+ <string>__TESTROOT__/Debug-iphonesimulator:__PLATFORMS__/iPhoneSimulator.platform/Developer/usr/lib</string>
83
+ <key>PERFC_SUPPRESS_SYSTEM_REPORTS</key>
84
+ <string>1</string>
85
+ <key>XCODE_SCHEME_NAME</key>
86
+ <string>UITreeServerUITests</string>
87
+ <key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
88
+ <string>__TESTROOT__/Debug-iphonesimulator</string>
89
+ <key>__XPC_DYLD_FRAMEWORK_PATH</key>
90
+ <string>__TESTROOT__/Debug-iphonesimulator</string>
91
+ <key>__XPC_DYLD_LIBRARY_PATH</key>
92
+ <string>__TESTROOT__/Debug-iphonesimulator</string>
93
+ </dict>
94
+ <key>ToolchainsSettingValue</key>
95
+ <array/>
96
+ <key>UITargetAppCommandLineArguments</key>
97
+ <array/>
98
+ <key>UITargetAppEnvironmentVariables</key>
99
+ <dict>
100
+ <key>APP_DISTRIBUTOR_ID_OVERRIDE</key>
101
+ <string>com.apple.AppStore</string>
102
+ <key>DYLD_FRAMEWORK_PATH</key>
103
+ <string>__TESTROOT__/Debug-iphonesimulator:__TESTROOT__/Debug-iphonesimulator/PackageFrameworks</string>
104
+ <key>DYLD_LIBRARY_PATH</key>
105
+ <string>__TESTROOT__/Debug-iphonesimulator</string>
106
+ <key>XCODE_SCHEME_NAME</key>
107
+ <string>UITreeServerUITests</string>
108
+ <key>__XCODE_BUILT_PRODUCTS_DIR_PATHS</key>
109
+ <string>__TESTROOT__/Debug-iphonesimulator</string>
110
+ <key>__XPC_DYLD_FRAMEWORK_PATH</key>
111
+ <string>__TESTROOT__/Debug-iphonesimulator</string>
112
+ <key>__XPC_DYLD_LIBRARY_PATH</key>
113
+ <string>__TESTROOT__/Debug-iphonesimulator</string>
114
+ </dict>
115
+ <key>UITargetAppPath</key>
116
+ <string>__TESTROOT__/Debug-iphonesimulator/UITreeServerApp.app</string>
117
+ <key>UITargetAppPerformanceAntipatternCheckerEnabled</key>
118
+ <true/>
119
+ <key>UserAttachmentLifetime</key>
120
+ <string>deleteOnSuccess</string>
121
+ </dict>
122
+ <key>__xctestrun_metadata__</key>
123
+ <dict>
124
+ <key>ContainerInfo</key>
125
+ <dict>
126
+ <key>ContainerName</key>
127
+ <string>UITreeServer</string>
128
+ <key>SchemeName</key>
129
+ <string>UITreeServerUITests</string>
130
+ </dict>
131
+ <key>FormatVersion</key>
132
+ <integer>1</integer>
133
+ </dict>
134
+ </dict>
135
+ </plist>
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@srmorete/mobile-device-mcp",
3
+ "version": "0.1.0",
4
+ "description": "An MCP server to control iOS and Android devices — Native and WebView, multi-device",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/srmorete/mobile-device-mcp.git"
9
+ },
10
+ "keywords": ["mcp", "mobile", "ios", "android", "automation"],
11
+ "type": "module",
12
+ "bin": {
13
+ "mobile-device-mcp": "./bin/cli.js"
14
+ },
15
+ "files": [
16
+ "bin/",
17
+ "src/",
18
+ "drivers/"
19
+ ],
20
+ "scripts": {
21
+ "start": "bun run src/server/index.ts",
22
+ "filter": "bun run src/filter/index.ts",
23
+ "test": "bun test",
24
+ "build": "./scripts/build.sh"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.12.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/bun": "^1.2.9"
31
+ }
32
+ }
@@ -0,0 +1,393 @@
1
+ import type {
2
+ AndroidNode,
3
+ Bounds,
4
+ FilteredElement,
5
+ IOSNode,
6
+ RawUITree,
7
+ } from "./types";
8
+
9
+ // --- 5.3 Type Mapping ---
10
+
11
+ const IOS_ELEMENT_TYPE_MAP: Record<number, string> = {
12
+ 8: "button",
13
+ 9: "button",
14
+ 11: "checkbox",
15
+ 32: "slider",
16
+ 37: "picker",
17
+ 39: "switch",
18
+ 41: "link",
19
+ 42: "image",
20
+ 44: "input",
21
+ 47: "text",
22
+ 48: "input",
23
+ 49: "input",
24
+ 51: "input",
25
+ 57: "webview",
26
+ };
27
+
28
+ const WEBVIEW_CLASS_MAP: Record<string, string> = {
29
+ "webview.Button": "button",
30
+ "webview.Link": "link",
31
+ "webview.Input": "input",
32
+ "webview.Select": "picker",
33
+ "webview.Checkbox": "checkbox",
34
+ "webview.Radio": "radio",
35
+ "webview.Switch": "switch",
36
+ "webview.Slider": "slider",
37
+ "webview.MenuItem": "menuitem",
38
+ "webview.Heading": "heading",
39
+ "webview.Paragraph": "text",
40
+ "webview.Text": "text",
41
+ "webview.List": "list",
42
+ "webview.ListItem": "listitem",
43
+ "webview.Table": "table",
44
+ "webview.TableRow": "tablerow",
45
+ "webview.TableCell": "tablecell",
46
+ "webview.Navigation": "navigation",
47
+ "webview.Main": "main",
48
+ "webview.Article": "article",
49
+ "webview.Section": "section",
50
+ "webview.Form": "form",
51
+ "webview.Dialog": "dialog",
52
+ "webview.Banner": "banner",
53
+ "webview.Footer": "footer",
54
+ "webview.Aside": "aside",
55
+ "webview.Image": "image",
56
+ "webview.Element": "webview",
57
+ };
58
+
59
+ const ANDROID_CLASS_MAP: Record<string, string> = {
60
+ "android.widget.Button": "button",
61
+ "android.widget.EditText": "input",
62
+ "android.widget.TextView": "text",
63
+ "android.widget.CheckBox": "checkbox",
64
+ "android.widget.Switch": "switch",
65
+ "android.widget.ImageView": "image",
66
+ "android.widget.ImageButton": "button",
67
+ "android.view.View": "container",
68
+ };
69
+
70
+ function resolveType(className: string | null): string {
71
+ if (className === null) return "unknown";
72
+
73
+ // Tier 1: iOS normalized (ios. prefix)
74
+ if (className.startsWith("ios.")) {
75
+ return className.slice(4);
76
+ }
77
+
78
+ // Tier 2: WebView semantic
79
+ if (className in WEBVIEW_CLASS_MAP) {
80
+ return WEBVIEW_CLASS_MAP[className];
81
+ }
82
+
83
+ // Tier 3: Android native
84
+ if (className in ANDROID_CLASS_MAP) {
85
+ return ANDROID_CLASS_MAP[className];
86
+ }
87
+
88
+ // Tier 4: Fallback — raw className as-is
89
+ return className;
90
+ }
91
+
92
+ // --- 5.7 Scoring ---
93
+
94
+ const TYPE_PRIORITY: Record<string, number> = {
95
+ button: 10,
96
+ link: 9,
97
+ input: 8,
98
+ heading: 7,
99
+ checkbox: 6,
100
+ switch: 6,
101
+ radio: 6,
102
+ slider: 6,
103
+ picker: 6,
104
+ menuitem: 5,
105
+ image: 3,
106
+ text: 2,
107
+ container: 1,
108
+ unknown: 0,
109
+ };
110
+
111
+ function semanticScore(element: FilteredElement): number {
112
+ return (element.clickable ? 100 : 0) + (TYPE_PRIORITY[element.type] ?? 0);
113
+ }
114
+
115
+ // --- 5.1 Platform Detection ---
116
+
117
+ function isIOSFormat(nodes: unknown[]): boolean {
118
+ if (nodes.length === 0) return false;
119
+ const first = nodes[0] as Record<string, unknown>;
120
+ return typeof first.elementType === "number" && first.frame != null;
121
+ }
122
+
123
+ // --- 5.2 iOS Normalization ---
124
+
125
+ const IOS_INPUT_TYPES = new Set(["input"]);
126
+ const IOS_CLICKABLE_TYPES = new Set(["button", "link", "input"]);
127
+
128
+ function normalizeIOSNode(node: IOSNode): AndroidNode {
129
+ const unifiedType = IOS_ELEMENT_TYPE_MAP[node.elementType] ?? "other";
130
+ const isInput = IOS_INPUT_TYPES.has(unifiedType);
131
+
132
+ // Text extraction: input types prefer value > label > title; others prefer label > value > title
133
+ let text: string | null;
134
+ if (isInput) {
135
+ text = node.value || node.label || node.title || null;
136
+ } else {
137
+ text = node.label || node.value || node.title || null;
138
+ }
139
+
140
+ const frame = node.frame;
141
+ const bounds: Bounds = {
142
+ left: frame.x,
143
+ top: frame.y,
144
+ right: frame.x + frame.width,
145
+ bottom: frame.y + frame.height,
146
+ };
147
+
148
+ const children = (node.children ?? []).map(normalizeIOSNode);
149
+
150
+ return {
151
+ className: `ios.${unifiedType}`,
152
+ text,
153
+ hintText: node.placeholderValue ?? null,
154
+ contentDesc: null,
155
+ resourceId: node.identifier || null,
156
+ packageName: null,
157
+ bounds,
158
+ checkable: false,
159
+ checked: false,
160
+ clickable: IOS_CLICKABLE_TYPES.has(unifiedType),
161
+ enabled: node.enabled,
162
+ focusable: false,
163
+ focused: node.hasFocus,
164
+ scrollable: false,
165
+ longClickable: false,
166
+ password: false,
167
+ selected: node.selected,
168
+ visibleToUser: true,
169
+ children,
170
+ };
171
+ }
172
+
173
+ // --- 5.4 Text Extraction ---
174
+
175
+ function extractText(node: AndroidNode): string {
176
+ return node.text || node.hintText || node.contentDesc || "";
177
+ }
178
+
179
+ // --- 5.5 Filter Predicates ---
180
+
181
+ function boundsAreZero(b: Bounds): boolean {
182
+ return b.left === 0 && b.top === 0 && b.right === 0 && b.bottom === 0;
183
+ }
184
+
185
+ function shouldFilter(node: AndroidNode, insideWebView: boolean): boolean {
186
+ // 1. WebView ancestry: inside a WebView AND not a webview-sourced element
187
+ if (insideWebView && node.source !== "webview") return true;
188
+
189
+ const b = node.bounds;
190
+
191
+ // 2. Null or all-zero bounds
192
+ if (b === null || boundsAreZero(b)) return true;
193
+
194
+ // 3. Zero or negative size
195
+ if (b.right - b.left <= 0 || b.bottom - b.top <= 0) return true;
196
+
197
+ return false;
198
+ }
199
+
200
+ function isWebViewContainer(node: AndroidNode): boolean {
201
+ return node.className === "android.webkit.WebView";
202
+ }
203
+
204
+ // --- 5.6 Tree Flattening ---
205
+
206
+ interface FlattenContext {
207
+ screenWidth: number;
208
+ screenHeight: number;
209
+ insideWebView: boolean;
210
+ }
211
+
212
+ function flattenTree(
213
+ nodes: AndroidNode[],
214
+ ctx: FlattenContext,
215
+ result: FilteredElement[],
216
+ idCounter: { value: number },
217
+ ): void {
218
+ for (const node of nodes) {
219
+ const childCtx: FlattenContext = isWebViewContainer(node)
220
+ ? { ...ctx, insideWebView: true }
221
+ : ctx;
222
+
223
+ if (!shouldFilter(node, ctx.insideWebView)) {
224
+ const text = extractText(node);
225
+ const clickable = node.clickable || node.longClickable;
226
+
227
+ if (text.length > 0 || clickable || node.source === "webview") {
228
+ const b = node.bounds;
229
+ if (b !== null) {
230
+ const centerX = (b.left + b.right) / 2;
231
+ const centerY = (b.top + b.bottom) / 2;
232
+ result.push({
233
+ id: idCounter.value,
234
+ text,
235
+ bounds: { left: b.left, top: b.top, right: b.right, bottom: b.bottom },
236
+ center: { x: Math.round(centerX), y: Math.round(centerY) },
237
+ type: resolveType(node.className),
238
+ visible:
239
+ centerX >= 0 &&
240
+ centerX <= ctx.screenWidth &&
241
+ centerY >= 0 &&
242
+ centerY <= ctx.screenHeight,
243
+ clickable,
244
+ });
245
+ idCounter.value++;
246
+ }
247
+ }
248
+ }
249
+
250
+ // Always process children, even if parent was filtered
251
+ flattenTree(node.children ?? [], childCtx, result, idCounter);
252
+ }
253
+ }
254
+
255
+ // --- 5.7 Deduplication ---
256
+
257
+ function exactDedup(elements: FilteredElement[]): FilteredElement[] {
258
+ const seen = new Set<string>();
259
+ const result: FilteredElement[] = [];
260
+ for (const el of elements) {
261
+ const key = `${el.text}|${el.bounds.left},${el.bounds.top},${el.bounds.right},${el.bounds.bottom}`;
262
+ if (!seen.has(key)) {
263
+ seen.add(key);
264
+ result.push(el);
265
+ }
266
+ }
267
+ return result;
268
+ }
269
+
270
+ function textHoisting(elements: FilteredElement[]): FilteredElement[] {
271
+ const claimed = new Set<number>(); // ids of elements already claimed as text sources
272
+ const toRemove = new Set<number>();
273
+
274
+ for (const parent of elements) {
275
+ if (!parent.clickable || parent.text) continue;
276
+
277
+ // Find non-clickable children with text whose bounds are contained within parent
278
+ const candidates: FilteredElement[] = [];
279
+ for (const child of elements) {
280
+ if (child.id === parent.id) continue;
281
+ if (child.clickable) continue;
282
+ if (!child.text) continue;
283
+ if (claimed.has(child.id)) continue;
284
+ if (toRemove.has(child.id)) continue;
285
+
286
+ // Check bounds containment (child inside parent)
287
+ if (
288
+ child.bounds.left >= parent.bounds.left &&
289
+ child.bounds.top >= parent.bounds.top &&
290
+ child.bounds.right <= parent.bounds.right &&
291
+ child.bounds.bottom <= parent.bounds.bottom
292
+ ) {
293
+ candidates.push(child);
294
+ }
295
+ }
296
+
297
+ if (candidates.length === 1) {
298
+ parent.text = candidates[0].text;
299
+ claimed.add(candidates[0].id);
300
+ toRemove.add(candidates[0].id);
301
+ }
302
+ }
303
+
304
+ return elements.filter((el) => !toRemove.has(el.id));
305
+ }
306
+
307
+ function boundsContain(outer: Bounds, inner: Bounds): boolean {
308
+ const tolerance = 5;
309
+ return (
310
+ outer.left - tolerance <= inner.left &&
311
+ outer.top - tolerance <= inner.top &&
312
+ outer.right + tolerance >= inner.right &&
313
+ outer.bottom + tolerance >= inner.bottom
314
+ );
315
+ }
316
+
317
+ function textOverlaps(a: string, b: string): boolean {
318
+ if (!a || !b) return false;
319
+ return a.includes(b) || b.includes(a);
320
+ }
321
+
322
+ function containmentDedup(elements: FilteredElement[]): FilteredElement[] {
323
+ const dropped = new Set<number>(); // indices
324
+
325
+ for (let i = 0; i < elements.length; i++) {
326
+ if (dropped.has(i)) continue;
327
+ const a = elements[i];
328
+
329
+ for (let j = i + 1; j < elements.length; j++) {
330
+ if (dropped.has(j)) continue;
331
+ const b = elements[j];
332
+
333
+ // Skip if no text overlap
334
+ if (!textOverlaps(a.text, b.text)) continue;
335
+
336
+ const aContainsB = boundsContain(a.bounds, b.bounds);
337
+ const bContainsA = boundsContain(b.bounds, a.bounds);
338
+
339
+ if (aContainsB && semanticScore(a) >= semanticScore(b)) {
340
+ dropped.add(j);
341
+ } else if (bContainsA && semanticScore(b) > semanticScore(a)) {
342
+ dropped.add(i);
343
+ break; // stop checking A against further elements
344
+ }
345
+ }
346
+ }
347
+
348
+ return elements.filter((_, idx) => !dropped.has(idx));
349
+ }
350
+
351
+ function reassignIds(elements: FilteredElement[]): FilteredElement[] {
352
+ for (let i = 0; i < elements.length; i++) {
353
+ elements[i].id = i + 1;
354
+ }
355
+ return elements;
356
+ }
357
+
358
+ // --- Main export ---
359
+
360
+ export function filterUITree(input: RawUITree): FilteredElement[] {
361
+ // 5.1: Validate required fields
362
+ if (input.screenWidth == null || input.screenHeight == null || !Array.isArray(input.nodes)) {
363
+ throw new Error(
364
+ "Invalid UI tree: missing required fields (screenWidth, screenHeight, nodes)",
365
+ );
366
+ }
367
+
368
+ let androidNodes: AndroidNode[];
369
+
370
+ // 5.1: Platform detection
371
+ if (input.nodes.length > 0 && isIOSFormat(input.nodes)) {
372
+ androidNodes = (input.nodes as IOSNode[]).map(normalizeIOSNode);
373
+ } else {
374
+ androidNodes = input.nodes as AndroidNode[];
375
+ }
376
+
377
+ // 5.6: Flatten
378
+ const ctx: FlattenContext = {
379
+ screenWidth: input.screenWidth,
380
+ screenHeight: input.screenHeight,
381
+ insideWebView: false,
382
+ };
383
+ const flat: FilteredElement[] = [];
384
+ flattenTree(androidNodes, ctx, flat, { value: 1 });
385
+
386
+ // 5.7: Deduplication
387
+ let result = exactDedup(flat);
388
+ result = textHoisting(result);
389
+ result = containmentDedup(result);
390
+ result = reassignIds(result);
391
+
392
+ return result;
393
+ }
@@ -0,0 +1,42 @@
1
+ import { filterUITree } from "./filter";
2
+ import type { RawUITree } from "./types";
3
+
4
+ async function main(): Promise<void> {
5
+ // Read all stdin as UTF-8
6
+ const chunks: Buffer[] = [];
7
+ for await (const chunk of Bun.stdin.stream()) {
8
+ chunks.push(Buffer.from(chunk));
9
+ }
10
+ const raw = Buffer.concat(chunks).toString("utf-8");
11
+
12
+ if (!raw.trim()) {
13
+ process.stderr.write("Error: empty input\n");
14
+ process.exit(1);
15
+ }
16
+
17
+ let parsed: RawUITree;
18
+ try {
19
+ parsed = JSON.parse(raw);
20
+ } catch (e) {
21
+ process.stderr.write(
22
+ `Error: invalid JSON — ${e instanceof Error ? e.message : e}\n`,
23
+ );
24
+ process.exit(1);
25
+ }
26
+
27
+ let elements;
28
+ try {
29
+ elements = filterUITree(parsed);
30
+ } catch (e) {
31
+ process.stderr.write(
32
+ `Error: ${e instanceof Error ? e.message : e}\n`,
33
+ );
34
+ process.exit(1);
35
+ }
36
+
37
+ for (const el of elements) {
38
+ process.stdout.write(JSON.stringify(el) + "\n");
39
+ }
40
+ }
41
+
42
+ main();
@@ -0,0 +1,70 @@
1
+ // Raw input types
2
+
3
+ export interface Bounds {
4
+ left: number;
5
+ top: number;
6
+ right: number;
7
+ bottom: number;
8
+ }
9
+
10
+ export interface Frame {
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ export interface IOSNode {
18
+ identifier: string;
19
+ label: string;
20
+ title?: string | null;
21
+ value?: string | null;
22
+ placeholderValue?: string | null;
23
+ elementType: number;
24
+ frame: Frame;
25
+ enabled: boolean;
26
+ selected: boolean;
27
+ hasFocus: boolean;
28
+ children?: IOSNode[] | null;
29
+ }
30
+
31
+ export interface AndroidNode {
32
+ className: string | null;
33
+ text: string | null;
34
+ hintText: string | null;
35
+ contentDesc: string | null;
36
+ resourceId: string | null;
37
+ packageName: string | null;
38
+ bounds: Bounds | null;
39
+ checkable: boolean;
40
+ checked: boolean;
41
+ clickable: boolean;
42
+ enabled: boolean;
43
+ focusable: boolean;
44
+ focused: boolean;
45
+ scrollable: boolean;
46
+ longClickable: boolean;
47
+ password: boolean;
48
+ selected: boolean;
49
+ visibleToUser: boolean;
50
+ children: AndroidNode[];
51
+ source?: "native" | "webview";
52
+ }
53
+
54
+ export interface RawUITree {
55
+ screenWidth: number;
56
+ screenHeight: number;
57
+ nodes: (IOSNode | AndroidNode)[];
58
+ }
59
+
60
+ // Output types
61
+
62
+ export interface FilteredElement {
63
+ id: number;
64
+ text: string;
65
+ bounds: Bounds;
66
+ center: { x: number; y: number };
67
+ type: string;
68
+ visible: boolean;
69
+ clickable: boolean;
70
+ }