@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.
- package/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/cli.js +2 -0
- package/drivers/android/app-debug-androidTest.apk +0 -0
- package/drivers/android/app-debug.apk +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.o +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingFox.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.o +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/FlyingSocks.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/PkgInfo +1 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/UITreeServerApp +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/UITreeServerApp.debug.dylib +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/_CodeSignature/CodeResources +128 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.app/__preview.dylib +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerApp.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/Testing +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/_CodeSignature/CodeResources +168 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/Testing.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/XCTAutomationSupport +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTAutomationSupport.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/XCTest +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/_CodeSignature/CodeResources +817 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTest.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/XCTestCore +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestCore.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/XCTestSupport +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCTestSupport.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/XCUIAutomation +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/_CodeSignature/CodeResources +432 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUIAutomation.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/XCUnit +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/_CodeSignature/CodeResources +113 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/XCUnit.framework/version.plist +18 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Frameworks/libXCTestSwiftSupport.dylib +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/Info.plist +254 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PkgInfo +1 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/Info.plist +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/UITreeServerUITests +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/PlugIns/UITreeServerUITests.xctest/_CodeSignature/CodeResources +101 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/UITreeServerUITests-Runner +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests-Runner.app/_CodeSignature/CodeResources +458 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/Project/arm64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/Project/x86_64-apple-ios-simulator.swiftsourceinfo +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/arm64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.abi.json +9 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/drivers/ios/Debug-iphonesimulator/UITreeServerUITests.swiftmodule/x86_64-apple-ios-simulator.swiftmodule +0 -0
- package/drivers/ios/UITreeServerUITests_iphonesimulator26.2-arm64-x86_64.xctestrun +135 -0
- package/drivers/ios/UITreeServerUITests_iphonesimulator26.2-arm64.xctestrun +135 -0
- package/package.json +32 -0
- package/src/filter/filter.ts +393 -0
- package/src/filter/index.ts +42 -0
- package/src/filter/types.ts +70 -0
- package/src/server/bootstrap.ts +367 -0
- package/src/server/devices.ts +262 -0
- package/src/server/index.ts +41 -0
- package/src/server/ports.ts +190 -0
- package/src/server/proxy.ts +119 -0
- package/src/server/tools.ts +303 -0
- 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
|
+
}
|