@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2
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 +201 -0
- package/NOTICE +7 -0
- package/README.md +306 -53
- package/bin/ai-app-bridge.js +51 -4504
- package/bin/android-permissions.js +152 -0
- package/bin/android-uia-xml.js +74 -0
- package/bin/artifact-paths.js +127 -1
- package/bin/bridge-forward.js +56 -0
- package/bin/command-discovery.js +86 -0
- package/bin/command-errors.js +60 -0
- package/bin/command-registry.js +504 -0
- package/bin/command-request.js +14 -0
- package/bin/command-router.js +80 -0
- package/bin/connection-cache.js +119 -0
- package/bin/device-provider.js +2934 -0
- package/bin/execution-host.js +596 -0
- package/bin/execution-runtime.js +101 -0
- package/bin/fact-codec.js +321 -0
- package/bin/fact-recorder.js +691 -0
- package/bin/fact-store.js +226 -0
- package/bin/feedback-probe.js +285 -0
- package/bin/intent/install-intent.js +267 -0
- package/bin/intent/intent-action-executor.js +129 -0
- package/bin/intent/intent-autonomous-adapter.js +46 -0
- package/bin/intent/intent-capture-port.js +64 -0
- package/bin/intent/intent-entry.js +226 -0
- package/bin/intent/intent-errors.js +28 -0
- package/bin/intent/intent-evidence-store.js +121 -0
- package/bin/intent/intent-lifetime.js +62 -0
- package/bin/intent/intent-observation-target.js +33 -0
- package/bin/intent/intent-observer.js +194 -0
- package/bin/intent/intent-production-adapter.js +334 -0
- package/bin/intent/intent-provider.js +27 -0
- package/bin/intent/intent-runtime.js +63 -0
- package/bin/intent/intent-worker.js +432 -0
- package/bin/intent/ios-intent-adapter.js +90 -0
- package/bin/intent/permission-intent.js +239 -0
- package/bin/intent/web-intent-adapter.js +31 -0
- package/bin/ios-device-outcome.js +47 -0
- package/bin/ios-execution.js +108 -0
- package/bin/ios-provider.js +497 -583
- package/bin/ios-runtime-binding.js +54 -0
- package/bin/ios-wda-execution.js +70 -0
- package/bin/ios-wda-port.js +98 -0
- package/bin/ios-wda-project.js +79 -0
- package/bin/mcp-server.js +83 -1040
- package/bin/mmap-scan-index.js +329 -0
- package/bin/observation-collector.js +862 -0
- package/bin/runtime-client.js +154 -0
- package/bin/runtime-directory.js +107 -0
- package/bin/runtime-protocol.js +36 -0
- package/bin/script/bounded-script-registry.js +103 -0
- package/bin/script/node-runtime-adapter.js +233 -0
- package/bin/script/progress-projector.js +63 -0
- package/bin/script/python-runtime-adapter.js +111 -0
- package/bin/script/rolling-summary.js +134 -0
- package/bin/script/script-agent-port.js +40 -0
- package/bin/script/script-assert.js +95 -0
- package/bin/script/script-capture-port.js +76 -0
- package/bin/script/script-catalog.js +85 -0
- package/bin/script/script-durable-restore.js +195 -0
- package/bin/script/script-entry-code.js +26 -0
- package/bin/script/script-entry-route.js +38 -0
- package/bin/script/script-entry.js +3 -0
- package/bin/script/script-errors.js +29 -0
- package/bin/script/script-evidence-store.js +22 -0
- package/bin/script/script-format-removed.js +26 -0
- package/bin/script/script-host-port.js +397 -0
- package/bin/script/script-ledger.js +64 -0
- package/bin/script/script-result.js +57 -0
- package/bin/script/script-sdk.js +152 -0
- package/bin/script/script-sdk.py +153 -0
- package/bin/script/script-session-channel.js +127 -0
- package/bin/script/script-spec.js +86 -0
- package/bin/script/script-supervisor.js +919 -0
- package/bin/script/templates/checkpoint-reentry.js +13 -0
- package/bin/segment-index.js +481 -0
- package/bin/segmented-fact-store.js +1571 -0
- package/bin/shared-kernel/android-h5-target.js +10 -0
- package/bin/shared-kernel/android-install-execution.js +176 -0
- package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
- package/bin/shared-kernel/android-shell-execution.js +195 -0
- package/bin/shared-kernel/argument-schema.js +117 -0
- package/bin/shared-kernel/canonical-path.js +17 -0
- package/bin/shared-kernel/device-acknowledgements.js +53 -0
- package/bin/shared-kernel/device-completion-history.js +52 -0
- package/bin/shared-kernel/device-mutation-lease.js +219 -0
- package/bin/shared-kernel/device-ownership-recovery.js +95 -0
- package/bin/shared-kernel/device-ownership-store.js +94 -0
- package/bin/shared-kernel/evidence-adapters.js +251 -0
- package/bin/shared-kernel/evidence-archive.js +329 -0
- package/bin/shared-kernel/evidence-recording.js +131 -0
- package/bin/shared-kernel/evidence-schema.js +194 -0
- package/bin/shared-kernel/evidence-store.js +190 -0
- package/bin/shared-kernel/execution-admission.js +22 -0
- package/bin/shared-kernel/execution-contracts.js +171 -0
- package/bin/shared-kernel/execution-io.js +106 -0
- package/bin/shared-kernel/execution-ledger.js +125 -0
- package/bin/shared-kernel/execution-scope.js +87 -0
- package/bin/shared-kernel/execution-target.js +115 -0
- package/bin/shared-kernel/flutter-execution.js +13 -0
- package/bin/shared-kernel/flutter-h5-port.js +60 -0
- package/bin/shared-kernel/flutter-h5-target.js +9 -0
- package/bin/shared-kernel/flutter-target.js +75 -0
- package/bin/shared-kernel/h5-execution.js +11 -0
- package/bin/shared-kernel/h5-target.js +31 -0
- package/bin/shared-kernel/host-fact-store.js +49 -0
- package/bin/shared-kernel/ios-h5-target.js +9 -0
- package/bin/shared-kernel/ios-native-target.js +71 -0
- package/bin/shared-kernel/live-capture-query.js +115 -0
- package/bin/shared-kernel/managed-sdk-execution.js +78 -0
- package/bin/shared-kernel/native-execution.js +13 -0
- package/bin/shared-kernel/native-target.js +156 -0
- package/bin/shared-kernel/provider-command-contracts.js +55 -0
- package/bin/shared-kernel/recorded-payload-archive.js +195 -0
- package/bin/shared-kernel/request-context.js +35 -0
- package/bin/shared-kernel/semantic-node.js +55 -0
- package/bin/shared-kernel/summary-transformer.js +352 -0
- package/bin/shared-kernel/target-lease-protocol.js +47 -0
- package/bin/shared-kernel/text-wait.js +111 -0
- package/bin/shared-kernel/uia-execution.js +96 -0
- package/bin/shared-kernel/uia-protocol.js +214 -0
- package/bin/shared-kernel/uia-runtime-port.js +377 -0
- package/bin/shared-kernel/uia-target.js +39 -0
- package/bin/shared-kernel/web-dom-target.js +44 -0
- package/bin/shared-kernel/xml-attributes.js +25 -0
- package/bin/target-execution.js +275 -0
- package/bin/web/command-schema.js +60 -0
- package/bin/web/session-store.js +157 -0
- package/bin/web-provider.js +334 -553
- package/docs/COMMAND_CONTRACT.md +1563 -0
- package/docs/EVIDENCE_ARCHIVE.md +214 -0
- package/docs/INTENT_FOREGROUND.md +71 -0
- package/docs/INTENT_NATIVE_EDITING.md +79 -0
- package/docs/RELEASE.md +59 -0
- package/docs/SCRIPT_AUTHORING.md +489 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
- package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
- package/package.json +60 -5
- package/runtime/ios-wda/AABWDABinding.h +19 -0
- package/runtime/ios-wda/AABWDABinding.m +97 -0
- package/runtime/ios-wda/AABWDAExecution.h +26 -0
- package/runtime/ios-wda/AABWDAExecution.m +172 -0
- package/runtime/ios-wda/AABWDAIntegration.h +71 -0
- package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
- package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
- package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
- package/runtime/uia/ai-app-bridge-uia.jar +0 -0
- package/runtime/uia/manifest.json +22 -0
- package/skills/ai-app-bridge-use/SKILL.md +23 -360
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { sdkCommands: iosSdkCommands } = require('./ios-runtime-binding');
|
|
4
|
+
|
|
5
|
+
const { CommandError } = require('./command-errors');
|
|
6
|
+
const { validateValue } = require('./shared-kernel/argument-schema');
|
|
7
|
+
const { executionCommandSchema, nativeGestureSchema, nativeSelector, flutterSelector } = require('./shared-kernel/execution-contracts');
|
|
8
|
+
const { flutterActionSchema, webCommandSchema } = require('./shared-kernel/provider-command-contracts');
|
|
9
|
+
|
|
10
|
+
const commandDefinitions = [
|
|
11
|
+
{ command: 'runtime', domain: 'execution', summary: 'Inspect, start or orderly stop the shared local execution runtime. CLI exit and MCP disconnect leave operations running; stop cancels and drains them.', targetKind: 'host-runtime', options: ['operation'] },
|
|
12
|
+
{ command: 'device-ownership', domain: 'execution', summary: 'Read device ownership, reconcile original completion and pending acknowledgements, or query a retained UIA receipt from Host FactStore by serial/runtimeEpoch/actionId. Never force-release or replay.', targetKind: 'android-device', options: ['operation', 'serial', 'timeoutMs', 'runtimeEpoch', 'actionId'] },
|
|
13
|
+
{ command: 'uia-runtime', domain: 'advanced', summary: 'Read, start or orderly stop the Android API 33+ UIA node runtime. Start checks the phone process lock; unacknowledged original receipts are retained.', targetKind: 'android-device', options: ['operation', 'serial', 'adb', 'timeoutMs'] },
|
|
14
|
+
{ command: 'status', domain: 'core', summary: 'Read bridge status, app/device metadata, capture counts, and Flutter summary.', targetApp: true, options: ['packageName', 'port', 'serial', 'full'] },
|
|
15
|
+
{ command: 'tree', domain: 'core', summary: 'Read Android View tree from the in-app bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes', 'maxDepth'] },
|
|
16
|
+
{ command: 'uia-tree', domain: 'core', summary: 'Read UIAutomator XML for the current foreground window.', options: ['serial', 'compact', 'textFilter', 'resourceIdFilter', 'classFilter', 'visibleOnly', 'maxNodes', 'maxDepth'] },
|
|
17
|
+
{ command: 'screenshot', domain: 'core', summary: 'Capture a screenshot, with foreground package verification when packageName is supplied.', options: ['serial', 'packageName', 'outFile', 'artifactDir'] },
|
|
18
|
+
{ command: 'logs', domain: 'core', summary: 'Read in-app log records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
19
|
+
{ command: 'network', domain: 'core', summary: 'Read in-app network records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'compact', 'urlFilter', 'method', 'statusCode', 'noBodies', 'bodyMaxBytes', 'sinceId', 'sinceMs', 'limit'] },
|
|
20
|
+
{ command: 'state', domain: 'core', summary: 'Read in-app state records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit'] },
|
|
21
|
+
{ command: 'events', domain: 'core', summary: 'Read in-app event records from the bridge.', targetApp: true, options: ['packageName', 'port', 'serial', 'sinceId', 'sinceMs', 'limit', 'includeActions'] },
|
|
22
|
+
{ command: 'logcat', domain: 'diagnostics', summary: 'Read Android logcat with optional app pid, tag, level, and grep filters.', options: ['serial', 'packageName', 'pid', 'appPid', 'tag', 'level', 'grep', 'lines', 'since', 'follow', 'durationSec', 'clear'] },
|
|
23
|
+
{ command: 'install-apk', domain: 'app', summary: 'Start a supervised Intent installation. Use intent observe/decide for actual system UI; completion binds the original phone job/session and independently verifies installed APK bytes. Requires Android SDK aapt/apksigner.', options: ['serial', 'packageName', 'apkPath', 'allowDowngrade', 'timeoutMs', 'adb', 'adbTimeoutMs', 'aaptPath', 'apksignerPath', 'recordingDir'] },
|
|
24
|
+
{ command: 'clear-app-data', domain: 'app', summary: 'Clear app data once through explicit method: pm-clear (default) or runtime. No fallback on failure.', targetApp: true, options: ['serial', 'packageName', 'method'] },
|
|
25
|
+
{ command: 'freeze-app', domain: 'app', summary: 'Optionally stop target app processes with SIGSTOP when dynamic UI needs stable evidence.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
|
|
26
|
+
{ command: 'thaw-app', domain: 'app', summary: 'Resume target app processes with SIGCONT before reads, waits, captures, actions, or final handoff.', targetApp: true, options: ['serial', 'packageName', 'pid'] },
|
|
27
|
+
{ command: 'launch-app', domain: 'app', summary: 'Launch the target package LAUNCHER Activity and report launcher candidates.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra', 'clearTask'] },
|
|
28
|
+
{ command: 'launch-activity', domain: 'app', summary: 'Launch an explicit Android Activity component with optional string extras.', targetApp: true, options: ['serial', 'packageName', 'activity', 'component', 'action', 'category', 'data', 'extra'] },
|
|
29
|
+
{ command: 'permission-state', domain: 'app', summary: 'Read live PackageManager runtime permission state for an explicit package and Android user.', targetApp: true, options: ['serial', 'packageName', 'permission', 'userId', 'adb', 'adbTimeoutMs', 'timeoutMs'] },
|
|
30
|
+
{ command: 'permission-grant', domain: 'app', summary: 'Grant a runtime permission as a fixture and verify the resulting PackageManager state.', targetApp: true, options: ['serial', 'packageName', 'permission', 'userId', 'adb', 'adbTimeoutMs', 'timeoutMs'] },
|
|
31
|
+
{ command: 'permission-revoke', domain: 'app', summary: 'Revoke a runtime permission as a fixture and verify the resulting PackageManager state.', targetApp: true, options: ['serial', 'packageName', 'permission', 'userId', 'adb', 'adbTimeoutMs', 'timeoutMs'] },
|
|
32
|
+
{ command: 'permission-dialog', domain: 'app', summary: 'Start an Intent for an already visible runtime permission request. Agent selects observed UI; PackageManager state and Activity closure verify allow, allow-once, deny or dismiss. Cancel stops the operation without closing the dialog.', targetApp: true, options: ['serial', 'packageName', 'permission', 'outcome', 'userId', 'timeoutMs', 'adb', 'adbTimeoutMs', 'recordingDir'] },
|
|
33
|
+
{ command: 'appops-set', domain: 'app', summary: 'Set an Android app-op mode.', targetApp: true, options: ['serial', 'packageName', 'op', 'mode'] },
|
|
34
|
+
{ command: 'tap', domain: 'action', summary: 'Tap physical coordinates. App scope uses the SDK; device scope uses ADB. An explicit package must match the foreground in either scope.', options: ['serial', 'tapX', 'tapY', 'scope'] },
|
|
35
|
+
{ command: 'tap-text', domain: 'action', summary: 'Observe then tap one exact text match. provider:auto selects Native, Flutter, then UIAutomator before one dispatch; an explicit provider pins Script replay.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'provider'] },
|
|
36
|
+
{ command: 'tap-uia-text', domain: 'action', summary: 'Revalidate one exact UIAutomator text match and foreground before device input.', options: ['serial', 'targetText', 'exact'] },
|
|
37
|
+
{ command: 'tap-uia', domain: 'action', summary: 'Tap one UIAutomator node by an exact semantic selector or an observed targetRef. Revalidate the original node and foreground before dispatch.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetRef'] },
|
|
38
|
+
{ command: 'tap-native', domain: 'action', summary: 'Tap one exact Native selector, optionally scoped to an observed row. Revalidate the View and foreground, then send a bound SDK action. Shared by CLI, MCP and Script.', targetApp: true, options: ['serial', 'packageName', 'selector'] },
|
|
39
|
+
{ command: 'wait-text', domain: 'action', summary: 'Wait for exact visible labels from one fresh foreground provider. Pure absence requires an explicit provider. Uses milliseconds.', targetApp: true, options: ['serial', 'packageName', 'targetText', 'provider', 'timeoutMs', 'intervalMs', 'requireText', 'absentText', 'requireActivity'] },
|
|
40
|
+
{ command: 'input-text', domain: 'action', summary: 'Replace native Android text, including empty or Unicode text. An exact selector binds the original editor; coordinates or the SDK focused-editor contract are separate targeting modes.', targetApp: true, options: ['serial', 'packageName', 'text', 'selector', 'tapX', 'tapY', 'hideKeyboard'] },
|
|
41
|
+
{ command: 'keyboard-state', domain: 'action', summary: 'Read Android soft keyboard visibility.', options: ['serial'] },
|
|
42
|
+
{ command: 'hide-keyboard', domain: 'action', summary: 'Hide the Android soft keyboard.', options: ['serial', 'force', 'intervalMs'] },
|
|
43
|
+
{ command: 'swipe', domain: 'action', summary: 'Swipe device coordinates through ADB.', options: ['serial', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
|
|
44
|
+
{ command: 'native-gesture', domain: 'action', summary: 'Long-press, swipe, or scroll an exact Native selector through the SDK, with a bound target and cancellable touch stream.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
|
|
45
|
+
{ command: 'keyevent', domain: 'action', summary: 'Send an Android keyevent through ADB.', options: ['serial', 'keyCode'] },
|
|
46
|
+
{ command: 'flutter-tree', domain: 'flutter', summary: 'Read the latest Flutter layout snapshot.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
47
|
+
{ command: 'flutter-nodes', domain: 'flutter', summary: 'Read Flutter operable nodes.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
48
|
+
{ command: 'flutter-action', domain: 'flutter', summary: 'Dispatch an explicit typed Flutter SDK action object. SDK actions remain expert primitives; use Intent for observed decisions.', targetApp: true, options: ['serial', 'packageName', 'payload'] },
|
|
49
|
+
{ command: 'tap-flutter', domain: 'flutter', summary: 'Tap a bound Flutter selector, or explicit logical coordinates. Supply selector or tapX/tapY; logical coordinates must not be multiplied by devicePixelRatio.', targetApp: true, options: ['serial', 'packageName', 'selector', 'tapX', 'tapY'] },
|
|
50
|
+
{ command: 'tap-flutter-text', domain: 'flutter', summary: 'Revalidate one exact Flutter label in the foreground activity, then tap in logical coordinates.', targetApp: true, options: ['serial', 'packageName', 'targetText'] },
|
|
51
|
+
{ command: 'input-flutter-text', domain: 'flutter', summary: 'Type into one bound EditableText selected by selector, coordinates, or the focused/unique editor. Revalidate the original editor and focus after frames; never redirect input.', targetApp: true, options: ['serial', 'packageName', 'text', 'selector', 'tapX', 'tapY', 'hideKeyboard'] },
|
|
52
|
+
{ command: 'scroll-flutter', domain: 'flutter', summary: 'Scroll a bound Flutter container by delta or until exact unique text is visible. Supply selector when several containers exist.', targetApp: true, options: ['serial', 'packageName', 'selector', 'targetText', 'delta', 'maxSwipes'] },
|
|
53
|
+
{ command: 'h5-dom', domain: 'webview', summary: 'Observe one visible Android WebView with stable page and element identities; ambiguity returns candidates.', targetApp: true, options: ['serial', 'packageName', 'port', 'webViewId'] },
|
|
54
|
+
{ command: 'h5-eval', domain: 'webview', summary: 'Execute synchronous expert JavaScript in the explicitly observed Android H5 page.', targetApp: true, options: ['serial', 'packageName', 'script', 'expectedPage'] },
|
|
55
|
+
{ command: 'h5-click', domain: 'webview', summary: 'Click a uniquely observed H5 element after document, geometry and native occlusion checks.', targetApp: true, options: ['serial', 'packageName', 'webViewId', 'selector', 'expectedTarget'] },
|
|
56
|
+
{ command: 'h5-input', domain: 'webview', summary: 'Replace H5 editor text, including empty text, with the original page and element binding.', targetApp: true, options: ['serial', 'packageName', 'webViewId', 'selector', 'text', 'expectedTarget'] },
|
|
57
|
+
{ command: 'h5-wait', domain: 'webview', summary: 'Observe until an exact H5 selector matches in the same WebView. Performs no JavaScript mutation.', targetApp: true, options: ['serial', 'packageName', 'webViewId', 'selector', 'timeoutMs', 'intervalMs'] },
|
|
58
|
+
{ command: 'h5-scroll', domain: 'webview', summary: 'Scroll an observed element into view, or scroll the bound page by explicit CSS pixel deltas.', targetApp: true, options: ['serial', 'packageName', 'webViewId', 'selector', 'expectedTarget', 'expectedPage', 'deltaX', 'deltaY'] },
|
|
59
|
+
{ command: 'flutter-h5-dom', domain: 'webview', summary: 'Observe one visible Flutter H5 adapter with stable registration, document and element identities; multiple visible adapters require adapterId.', targetApp: true, options: ['serial', 'packageName', 'port', 'adapterId'] },
|
|
60
|
+
{ command: 'flutter-h5-eval', domain: 'webview', summary: 'Execute synchronous expert JavaScript in an explicitly observed Flutter H5 adapter document.', targetApp: true, options: ['serial', 'packageName', 'script', 'expectedPage'] },
|
|
61
|
+
{ command: 'flutter-h5-click', domain: 'webview', summary: 'Click one exact Flutter H5 element with adapter, document, geometry and DOM occlusion checks.', targetApp: true, options: ['serial', 'packageName', 'adapterId', 'selector', 'expectedTarget'] },
|
|
62
|
+
{ command: 'flutter-h5-input', domain: 'webview', summary: 'Replace text in one bound Flutter H5 editor; revalidate the original element after focus callbacks.', targetApp: true, options: ['serial', 'packageName', 'adapterId', 'selector', 'text', 'expectedTarget'] },
|
|
63
|
+
{ command: 'flutter-h5-wait', domain: 'webview', summary: 'Observe an exact selector in the same Flutter H5 adapter document; never switch pages or execute an eval mutation.', targetApp: true, options: ['serial', 'packageName', 'adapterId', 'selector', 'timeoutMs', 'intervalMs'] },
|
|
64
|
+
{ command: 'flutter-h5-scroll', domain: 'webview', summary: 'Scroll a bound Flutter H5 element into view or scroll its page by explicit CSS deltas.', targetApp: true, options: ['serial', 'packageName', 'adapterId', 'selector', 'expectedTarget', 'expectedPage', 'deltaX', 'deltaY'] },
|
|
65
|
+
{ command: 'webview-pages', domain: 'webview', summary: 'List attachable Android WebView DevTools/CDP pages.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'keepForward'] },
|
|
66
|
+
{ command: 'webview-network', domain: 'webview', summary: 'Capture WebView Network events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'urlFilter', 'durationMs', 'script', 'includeResponseBody', 'bodyMaxBytes', 'maxEvents'] },
|
|
67
|
+
{ command: 'webview-console', domain: 'webview', summary: 'Capture WebView console/log events through CDP.', targetApp: true, options: ['serial', 'packageName', 'webviewPort', 'socketName', 'targetId', 'pageUrlFilter', 'durationMs', 'script', 'maxEvents'] },
|
|
68
|
+
{ command: 'ios-doctor', domain: 'ios', summary: 'Check Xcode, connected iPhone, AiAppBridgeIOS runtime, and WebDriverAgent readiness.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'wdaUrl', 'wdaRunnerBundleId'] },
|
|
69
|
+
{ command: 'ios-setup', domain: 'ios', summary: 'Verify iOS setup; can install/launch the app and start the prepared bound WDA runtime when signing inputs are supplied.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'appPath', 'iosHost', 'iosPort', 'runtimeUrl', 'wdaUrl', 'wdaRunnerBundleId', 'wdaTestBundleId', 'teamId', 'startWda'] },
|
|
70
|
+
{ command: 'ios-devices', domain: 'ios', summary: 'List iOS devices known to xcrun devicectl.', targetKind: 'ios-device', options: ['deviceId'] },
|
|
71
|
+
{ command: 'ios-install-app', domain: 'ios', summary: 'Install an iOS .app bundle through devicectl.', targetKind: 'ios-app', options: ['deviceId', 'appPath'] },
|
|
72
|
+
{ command: 'ios-launch-app', domain: 'ios', summary: 'Launch an iOS app by bundle identifier through devicectl.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'terminateExisting'] },
|
|
73
|
+
{ command: 'ios-execution', domain: 'ios', summary: 'Read SDK or WDA execution/physical ownership, cancel an original action, read its durable completion, or reconcile unknown ownership without replay.', targetKind: 'ios-app', options: ['operation', 'deviceId', 'bundleId', 'kind', 'actionId', 'runtimeEpoch', 'runtimeUrl', 'iosHost', 'iosPort', 'wdaRunnerBundleId', 'wdaUrl', 'devicectl', 'timeoutMs'] },
|
|
74
|
+
{ command: 'ios-status', domain: 'ios', summary: 'Read AiAppBridgeIOS runtime status.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
|
|
75
|
+
{ command: 'ios-tree', domain: 'ios', summary: 'Read UIKit tree from the AiAppBridgeIOS runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
|
|
76
|
+
{ command: 'ios-logs', domain: 'ios', summary: 'Read in-app iOS log records.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit'] },
|
|
77
|
+
{ command: 'ios-network', domain: 'ios', summary: 'Read in-app iOS network records.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit'] },
|
|
78
|
+
{ command: 'ios-state', domain: 'ios', summary: 'Read in-app iOS state records. More than 200 keys use a byte-bounded keyed LRU; the normal range stays equivalent.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit'] },
|
|
79
|
+
{ command: 'ios-events', domain: 'ios', summary: 'Read in-app iOS event records.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'sinceId', 'sinceMs', 'limit', 'includeActions'] },
|
|
80
|
+
{ command: 'ios-h5-dom', domain: 'ios', summary: 'Read a unique visible WKWebView and its bound document/element identities. Select webViewId explicitly when multiple views are visible.', targetKind: 'ios-app', options: ['webViewId'] },
|
|
81
|
+
{ command: 'ios-h5-eval', domain: 'ios', summary: 'Execute expert JavaScript in an explicitly observed WKWebView document, with original execution receipts.', targetKind: 'ios-app', options: ['expectedPage', 'script'] },
|
|
82
|
+
{ command: 'ios-h5-click', domain: 'ios', summary: 'Click one exact DOM element in a bound visible WKWebView document.', targetKind: 'ios-app', options: ['webViewId', 'selector', 'expectedTarget'] },
|
|
83
|
+
{ command: 'ios-h5-input', domain: 'ios', summary: 'Replace one exact DOM editor value in a bound WKWebView document.', targetKind: 'ios-app', options: ['webViewId', 'selector', 'expectedTarget', 'text'] },
|
|
84
|
+
{ command: 'ios-h5-scroll', domain: 'ios', summary: 'Scroll an observed DOM element into view in the same bound WKWebView document.', targetKind: 'ios-app', options: ['webViewId', 'selector', 'expectedTarget'] },
|
|
85
|
+
{ command: 'ios-flutter-tree', domain: 'ios', summary: 'Read Flutter iOS layout snapshot from the runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
|
|
86
|
+
{ command: 'ios-flutter-nodes', domain: 'ios', summary: 'Read Flutter iOS operable nodes from the runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl'] },
|
|
87
|
+
{ command: 'ios-flutter-action', domain: 'ios', summary: 'Dispatch a Flutter iOS action through the runtime.', targetKind: 'ios-app', options: ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'payload'] },
|
|
88
|
+
{ command: 'ios-tap-flutter', domain: 'ios', summary: 'Tap one exact Flutter iOS node using a live Element reference.', targetKind: 'ios-app', options: ['selector'] },
|
|
89
|
+
{ command: 'ios-input-flutter-text', domain: 'ios', summary: 'Replace text in one exact Flutter iOS editor.', targetKind: 'ios-app', options: ['selector', 'text'] },
|
|
90
|
+
{ command: 'ios-scroll-flutter', domain: 'ios', summary: 'Scroll one exact Flutter iOS container by a logical-pixel delta.', targetKind: 'ios-app', options: ['selector', 'delta'] },
|
|
91
|
+
{ command: 'ios-flutter-back', domain: 'ios', summary: 'Ask the bound Flutter iOS navigator to go back once.', targetKind: 'ios-app', options: [] },
|
|
92
|
+
{ command: 'ios-flutter-hide-keyboard', domain: 'ios', summary: 'Unfocus the bound Flutter iOS app and request keyboard dismissal. Reobserve viewport.viewInsets to verify dismissal.', targetKind: 'ios-app', options: [] },
|
|
93
|
+
{ command: 'ios-screenshot', domain: 'ios', summary: 'Capture an iOS device screenshot through devicectl.', targetKind: 'ios-device', options: ['deviceId', 'outFile', 'artifactDir', 'displayUniqueId'] },
|
|
94
|
+
{ command: 'ios-wda-status', domain: 'ios', summary: 'Check the WDA instance bound to the selected device Runner container.', targetKind: 'ios-device', options: ['deviceId', 'wdaRunnerBundleId', 'wdaUrl'] },
|
|
95
|
+
{ command: 'ios-wda-session', domain: 'ios', summary: 'Explicitly create, inspect, or close a WDA session attached to an already foreground App; never launch an App or replace another session.', targetKind: 'ios-app', options: ['operation', 'deviceId', 'wdaRunnerBundleId', 'wdaUrl', 'bundleId', 'wdaSessionId'] },
|
|
96
|
+
{ command: 'ios-uia-tree', domain: 'ios', summary: 'Read the bound App session XCUITest tree.', targetKind: 'ios-app', options: ['bundleId', 'wdaUrl', 'wdaSessionId'] },
|
|
97
|
+
{ command: 'ios-tap', domain: 'ios', summary: 'Tap coordinates in the bound foreground App session.', targetKind: 'ios-app', options: ['bundleId', 'wdaUrl', 'wdaSessionId', 'tapX', 'tapY'] },
|
|
98
|
+
{ command: 'ios-tap-native', domain: 'ios', summary: 'Tap one visible native iOS control by exact accessibilityId, label or observed elementId, bound to its WDA session and element identity.', targetKind: 'ios-app', options: ['selector', 'expectedTarget'] },
|
|
99
|
+
{ command: 'ios-input-native-text', domain: 'ios', summary: 'Replace text in one exact native iOS editor with live identity and focus checks.', targetKind: 'ios-app', options: ['selector', 'text', 'expectedTarget'] },
|
|
100
|
+
{ command: 'ios-input', domain: 'ios', summary: 'Type into exactly one explicit WDA element or unique accessibility ID in the bound App session.', targetKind: 'ios-app', options: ['bundleId', 'wdaUrl', 'wdaSessionId', 'text', 'accessibilityId', 'elementId', 'clearFirst'] },
|
|
101
|
+
{ command: 'ios-swipe', domain: 'ios', summary: 'Swipe coordinates in the bound foreground App session.', targetKind: 'ios-app', options: ['bundleId', 'wdaUrl', 'wdaSessionId', 'startX', 'startY', 'endX', 'endY', 'durationMs'] },
|
|
102
|
+
{ command: 'ios-set-orientation', domain: 'ios', summary: 'Rotate the device for a requested App interface orientation in the bound foreground session. The original XCTest completion reports the observed interface direction; reobserve the UI before interacting.', targetKind: 'ios-app', options: ['orientation', 'expectedSession'] },
|
|
103
|
+
{ command: 'web-provider-status', domain: 'web', summary: 'Read desktop Web Bridge provider status.', options: [] },
|
|
104
|
+
{ command: 'web-session-start', domain: 'web', summary: 'Start the desktop Web Bridge WebSocket session server.', options: ['host', 'webPort', 'path', 'token'] },
|
|
105
|
+
{ command: 'web-session-stop', domain: 'web', summary: 'Close this Host Web server and its sockets; unresolved actions retain ownership.', options: [] },
|
|
106
|
+
{ command: 'web-execution', domain: 'web', summary: 'Read, cancel or recover the original Web action using committed completion facts.', targetKind: 'web-target', options: ['operation', 'sessionId', 'runtimeEpoch', 'targetId', 'actionId', 'timeoutMs'] },
|
|
107
|
+
{ command: 'web-connect-info', domain: 'web', summary: 'Read the Web Bridge endpoint and token for SDK clients.', options: [] },
|
|
108
|
+
{ command: 'web-sessions', domain: 'web', summary: 'List connected Web Bridge SDK sessions.', options: [] },
|
|
109
|
+
{ command: 'web-status', domain: 'web', summary: 'Read status and capture counts for a Web Bridge session.', targetKind: 'web-target', options: ['sessionId'] },
|
|
110
|
+
{ command: 'web-dom', domain: 'web', summary: 'Read the current bound document DOM, or explicit committed DOM history.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'selector', 'maxControls', 'timeoutMs', 'history', 'cursor', 'throughCursor', 'sinceMs', 'limit'] },
|
|
111
|
+
{ command: 'web-logs', domain: 'web', summary: 'Read committed Web log windows or explicit history.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'view', 'factCursor', 'afterActionId', 'history', 'cursor', 'throughCursor', 'sinceMs', 'limit'] },
|
|
112
|
+
{ command: 'web-network', domain: 'web', summary: 'Read committed Web network windows or explicit history.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'view', 'factCursor', 'afterActionId', 'history', 'cursor', 'throughCursor', 'sinceMs', 'limit'] },
|
|
113
|
+
{ command: 'web-state', domain: 'web', summary: 'Read committed Web state windows or explicit history.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'view', 'factCursor', 'afterActionId', 'history', 'cursor', 'throughCursor', 'sinceMs', 'limit'] },
|
|
114
|
+
{ command: 'web-events', domain: 'web', summary: 'Read committed Web event windows or explicit history.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'view', 'factCursor', 'afterActionId', 'history', 'cursor', 'throughCursor', 'sinceMs', 'limit'] },
|
|
115
|
+
{ command: 'web-command', domain: 'web', summary: 'Run a typed built-in Web SDK command, or action with an explicit registered App action name and JSON arguments.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'name', 'arguments', 'timeoutMs'] },
|
|
116
|
+
{ command: 'web-click', domain: 'web', summary: 'Click one visible DOM element, optionally bound to its observed page and element identity.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'selector', 'expectedTarget', 'timeoutMs'] },
|
|
117
|
+
{ command: 'web-input', domain: 'web', summary: 'Replace text in a form input or contenteditable editor through browser input events.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'selector', 'expectedTarget', 'value', 'timeoutMs'] },
|
|
118
|
+
{ command: 'web-key', domain: 'web', summary: 'Dispatch one Enter or Escape keydown event to one observed DOM control.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'selector', 'expectedTarget', 'key', 'timeoutMs'] },
|
|
119
|
+
{ command: 'web-wait', domain: 'web', summary: 'Wait for text or selector through the Web Bridge SDK command path.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'selector', 'targetText', 'timeoutMs'] },
|
|
120
|
+
{ command: 'web-scroll', domain: 'web', summary: 'Bring one DOM element into view or scroll an element or window by explicit deltas.', targetKind: 'web-target', options: ['sessionId', 'runtimeEpoch', 'targetId', 'mode', 'selector', 'expectedTarget', 'deltaX', 'deltaY', 'timeoutMs'] },
|
|
121
|
+
{ command: 'forward', domain: 'advanced', summary: 'Create the ADB port forward for the bridge.', targetApp: true, options: ['serial', 'packageName', 'port'] },
|
|
122
|
+
{ command: 'remove-forward', domain: 'advanced', summary: 'Remove the ADB port forward for the bridge.', options: ['serial', 'port'] },
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const isolatedCommandDefinitions = [
|
|
126
|
+
{
|
|
127
|
+
command: 'script',
|
|
128
|
+
domain: 'execution',
|
|
129
|
+
summary: 'Run a trusted-local-code JavaScript or Python Script. Operations: start, status, wait, pause, resume, decide, cancel, runtime-status. Commands and assertions return results for Script code to handle; uncaught errors fail execution. Script source is not an OS sandbox.',
|
|
130
|
+
options: ['operation', 'waitMs', 'afterSequence', 'recordingDir'],
|
|
131
|
+
runtime: 'trusted-local-code',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
command: 'intent',
|
|
135
|
+
domain: 'execution',
|
|
136
|
+
summary: 'Run an Intent: start, status, observe, decide, pause, resume, cancel, or intervene. The total deadline includes observation and decision waits. Cancel drains owned work before persisting the terminal outcome; status recovers durable outcomes after restart without replay. target.foregroundPackages explicitly enables Android foreground provider routing.',
|
|
137
|
+
options: ['operation', 'recordingDir'],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
command: 'evidence',
|
|
141
|
+
domain: 'evidence',
|
|
142
|
+
summary: 'Export retained Host evidence for one operation; includeRecordedPayloads adds files captured with start.recordingDir. Verify archives offline against their frozen manifest SHA-256 without opening FactStore or contacting devices.',
|
|
143
|
+
options: ['operation', 'namespace', 'operationId', 'outputDir', 'includeRecordedPayloads', 'archiveDir', 'manifestSha256'],
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const mutationCommands = new Set([
|
|
148
|
+
'install-apk', 'clear-app-data', 'freeze-app', 'thaw-app', 'launch-app',
|
|
149
|
+
'launch-activity', 'permission-grant',
|
|
150
|
+
'permission-revoke', 'permission-dialog', 'appops-set',
|
|
151
|
+
'tap', 'tap-text', 'tap-uia-text', 'tap-uia', 'tap-native', 'input-text', 'hide-keyboard', 'swipe', 'native-gesture', 'keyevent',
|
|
152
|
+
'flutter-action', 'tap-flutter', 'tap-flutter-text', 'input-flutter-text', 'scroll-flutter',
|
|
153
|
+
'h5-eval', 'h5-click', 'h5-input', 'h5-scroll',
|
|
154
|
+
'flutter-h5-eval', 'flutter-h5-click', 'flutter-h5-input', 'flutter-h5-scroll',
|
|
155
|
+
'ios-setup', 'ios-install-app', 'ios-launch-app', 'ios-h5-eval', 'ios-h5-click', 'ios-h5-input', 'ios-h5-scroll', 'ios-flutter-action',
|
|
156
|
+
'ios-tap', 'ios-input', 'ios-swipe', 'ios-set-orientation',
|
|
157
|
+
'ios-tap-native', 'ios-input-native-text',
|
|
158
|
+
'ios-tap-flutter', 'ios-input-flutter-text', 'ios-scroll-flutter', 'ios-flutter-back', 'ios-flutter-hide-keyboard',
|
|
159
|
+
'web-session-start', 'web-session-stop', 'web-command', 'web-click', 'web-input', 'web-key', 'web-scroll', 'web-wait',
|
|
160
|
+
]);
|
|
161
|
+
const historyCommands = new Set([
|
|
162
|
+
'tree', 'uia-tree', 'flutter-tree', 'flutter-nodes', 'screenshot', 'h5-dom', 'logcat',
|
|
163
|
+
'logs', 'network', 'state', 'events', 'ios-tree', 'ios-uia-tree', 'ios-screenshot',
|
|
164
|
+
'ios-h5-dom', 'ios-flutter-tree', 'ios-flutter-nodes', 'ios-logs', 'ios-network',
|
|
165
|
+
'ios-state', 'ios-events', 'web-dom', 'web-logs', 'web-network', 'web-state', 'web-events',
|
|
166
|
+
]);
|
|
167
|
+
const captureCommands = new Set(['logs', 'network', 'state', 'events', 'ios-logs', 'ios-network', 'ios-state', 'ios-events']);
|
|
168
|
+
const androidCommon = ['adb', 'adbTimeoutMs', 'serial', 'packageName', 'port', 'timeoutMs'];
|
|
169
|
+
const iosCommon = ['deviceId', 'bundleId', 'iosHost', 'iosPort', 'runtimeUrl', 'wdaUrl', 'wdaSessionId', 'devicectl', 'xcodebuild', 'timeoutMs'];
|
|
170
|
+
const executionOptions = ['requestId', 'feedback'];
|
|
171
|
+
const requiredOptions = {
|
|
172
|
+
tap: ['tapX', 'tapY'], 'tap-flutter': ['packageName'],
|
|
173
|
+
'tap-text': ['targetText'], 'tap-uia-text': ['targetText'], 'tap-uia': ['packageName'], 'tap-native': ['packageName', 'selector'], 'tap-flutter-text': ['targetText'],
|
|
174
|
+
'wait-text': [], 'input-text': ['text'], 'input-flutter-text': ['text'],
|
|
175
|
+
swipe: ['startX', 'startY', 'endX', 'endY'],
|
|
176
|
+
'native-gesture': ['packageName', 'payload'],
|
|
177
|
+
'install-apk': ['apkPath'], 'clear-app-data': ['packageName'],
|
|
178
|
+
'freeze-app': ['packageName'], 'thaw-app': ['packageName'],
|
|
179
|
+
'permission-state': ['serial', 'packageName', 'permission'], 'permission-grant': ['packageName', 'permission'],
|
|
180
|
+
'permission-revoke': ['packageName', 'permission'], 'permission-dialog': ['packageName', 'permission', 'outcome'], 'appops-set': ['packageName', 'op', 'mode'],
|
|
181
|
+
'h5-eval': ['script', 'expectedPage'], 'h5-click': ['selector'], 'h5-wait': ['selector'], 'flutter-h5-eval': ['script', 'expectedPage'], 'ios-h5-eval': ['script', 'expectedPage'], 'ios-h5-click': ['selector'], 'ios-h5-input': ['selector', 'text'], 'ios-h5-scroll': ['selector'],
|
|
182
|
+
'h5-input': ['selector', 'text'], 'flutter-h5-input': ['selector', 'text'], 'flutter-h5-click': ['selector'], 'flutter-h5-wait': ['selector'],
|
|
183
|
+
'flutter-action': ['payload'], 'ios-flutter-action': ['payload'],
|
|
184
|
+
'ios-install-app': ['appPath'], 'ios-launch-app': ['bundleId'],
|
|
185
|
+
'ios-tap': ['tapX', 'tapY'], 'ios-input': ['text'],
|
|
186
|
+
'ios-tap-native': ['selector'], 'ios-input-native-text': ['selector', 'text'],
|
|
187
|
+
'ios-swipe': ['startX', 'startY', 'endX', 'endY'],
|
|
188
|
+
'ios-set-orientation': ['orientation'],
|
|
189
|
+
'ios-tap-flutter': ['selector'], 'ios-input-flutter-text': ['selector', 'text'], 'ios-scroll-flutter': ['selector', 'delta'],
|
|
190
|
+
'web-command': ['name'], 'web-input': ['selector', 'value'],
|
|
191
|
+
};
|
|
192
|
+
const optionTypes = {};
|
|
193
|
+
function define(names, schema) {
|
|
194
|
+
for (const name of names.split(' ')) optionTypes[name] = schema;
|
|
195
|
+
}
|
|
196
|
+
define('wdaRunnerBundleId wdaTestBundleId', { type: 'string', minLength: 1, maxLength: 255, pattern: '^[A-Za-z0-9_.-]+$' });
|
|
197
|
+
define('adapterId webViewId', { type: 'string', minLength: 1 });
|
|
198
|
+
define('expectedPage', require('./shared-kernel/ios-h5-target').pageSchema);
|
|
199
|
+
define('expectedTarget', require('./shared-kernel/ios-h5-target').expectedTargetSchema);
|
|
200
|
+
define('expectedSession', require('./shared-kernel/ios-native-target').expectedSessionSchema);
|
|
201
|
+
define('orientation', require('./shared-kernel/ios-native-target').orientationSchema);
|
|
202
|
+
define('selector', { type: 'string', minLength: 1 });
|
|
203
|
+
define('spec', { type: 'object', additionalProperties: true });
|
|
204
|
+
define('targetRef', require('./shared-kernel/uia-protocol').targetRefSchema);
|
|
205
|
+
define('adb serial packageName artifactDir deviceId bundleId iosHost runtimeUrl wdaUrl wdaSessionId devicectl xcodebuild host path token appPath apkPath activity component action data initialRoute outFile permission op mode targetText requireText absentText requireActivity resourceId textFilter resourceIdFilter classFilter urlFilter method pageUrlFilter socketName targetId sessionId accessibilityId elementId wdaProjectPath wdaBundleId teamId displayUniqueId tag level grep since factCursor cursor runtimeEpoch afterActionId mobileFactId targetKey requestId deviceLogScope logcatFormat format agentModule pythonPath goal intent operationId recordingDir namespace outputDir archiveDir manifestSha256 name reason provider adapter', { type: 'string', minLength: 1 });
|
|
206
|
+
define('text value script payload', { type: 'string' });
|
|
207
|
+
define('aaptPath apksignerPath', { type: 'string', minLength: 1 });
|
|
208
|
+
define('full compact visibleOnly clear follow appPid force hideKeyboard noAutoHideKeyboard allowDowngrade clearTask exact skipFlutterLaunch includeResponseBody keepForward noBodies startWda terminateExisting clearFirst refresh includeActions history includeRecordedPayloads grepCaseSensitive includeCatalog', { type: 'boolean' });
|
|
209
|
+
define('tapX tapY startX startY endX endY', { type: 'number', minimum: 0, maximum: 2147483647, description: 'Finite non-negative coordinate. Space is specified by the command.' });
|
|
210
|
+
define('delta deltaX deltaY', { type: 'number', minimum: -2147483647, maximum: 2147483647 });
|
|
211
|
+
define('port iosPort webviewPort webPort', { type: 'integer', minimum: 1, maximum: 65535 });
|
|
212
|
+
define('webPort', { type: 'integer', minimum: 0, maximum: 65535, description: '0 asks the OS to assign an available local port.' });
|
|
213
|
+
define('adbTimeoutMs httpTimeoutMs timeoutMs installTimeoutMs installerTimeoutMs intervalMs durationMs', { type: 'integer', minimum: 1, maximum: 2147483647, description: 'Milliseconds.' });
|
|
214
|
+
define('waitMs', { type: 'integer', minimum: 0, maximum: 60000, description: 'Milliseconds; 0 polls immediately.' });
|
|
215
|
+
define('timeoutSec durationSec', { type: 'number', exclusiveMinimum: 0, maximum: 2147483, description: 'Seconds. Retained explicit legacy unit.' });
|
|
216
|
+
define('sinceId sinceMs afterSequence revision', { type: 'integer', minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
|
|
217
|
+
define('limit eventLimit pid lines attempts maxSwipes', { type: 'integer', minimum: 1, maximum: 2147483647 });
|
|
218
|
+
define('maxNodes maxDepth maxEvents bodyMaxBytes', { type: 'integer', minimum: 0, maximum: 2147483647 });
|
|
219
|
+
define('keyCode', { type: 'integer', minimum: 0, maximum: 2147483647, default: 4 });
|
|
220
|
+
define('statusCode', { type: 'integer', minimum: 100, maximum: 599 });
|
|
221
|
+
define('category extra deviceLogBuffers', { anyOf: [{ type: 'string', minLength: 1 }, { type: 'array', items: { type: 'string', minLength: 1 } }] });
|
|
222
|
+
define('arguments target budget require decision', { type: 'object', additionalProperties: true });
|
|
223
|
+
define('feedback', { enum: ['auto', 'off', 'full'], description: 'auto/off/full.' });
|
|
224
|
+
define('view', { enum: ['legacy-live', 'decision-window', 'connected-history'] });
|
|
225
|
+
define('userId', { type: 'integer', minimum: 0, maximum: 2147483647, description: 'Defaults to the actual current Android user, frozen before mutation.' });
|
|
226
|
+
define('outcome', { enum: ['allow', 'allow-once', 'deny', 'dismiss'] });
|
|
227
|
+
define('operation', { type: 'string', minLength: 1 });
|
|
228
|
+
define('scope', { enum: ['app', 'device'], description: 'Defaults to app with packageName, otherwise device. App scope requires the SDK and does not fall back to ADB.' });
|
|
229
|
+
define('deviceLogScope', { enum: ['device'] });
|
|
230
|
+
define('history', { type: 'boolean', description: 'Mobile streams read the phone FactStore while connected; disconnected targets never use Host-copied payload. UI/Web history reads retained Host facts.' });
|
|
231
|
+
|
|
232
|
+
const extraOptions = {
|
|
233
|
+
logcat: ['deviceLogScope', 'deviceLogBuffers', 'cursor', 'format', 'grepCaseSensitive'],
|
|
234
|
+
};
|
|
235
|
+
const commandByName = new Map(commandDefinitions.map(d => [d.command, d]));
|
|
236
|
+
const isolatedByName = new Map(isolatedCommandDefinitions.map(d => [d.command, d]));
|
|
237
|
+
|
|
238
|
+
function isMutationCommand(command, args = {}) {
|
|
239
|
+
if (command === 'web-command' && args.name === 'domSnapshot') return false;
|
|
240
|
+
return mutationCommands.has(command) || (command === 'logcat' && args.clear === true)
|
|
241
|
+
|| (command === 'ios-wda-session' && args.operation !== 'status')
|
|
242
|
+
|| (command === 'uia-runtime' && args.operation !== 'status')
|
|
243
|
+
|| ((command === 'webview-network' || command === 'webview-console') && args.script !== undefined);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isAndroidMutation(command, args = {}) {
|
|
247
|
+
return isMutationCommand(command, args) && command !== 'runtime' && !command.startsWith('ios-') && !command.startsWith('web-');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function executionTimeoutMs(command, args = {}) {
|
|
251
|
+
if (args.timeoutMs !== undefined) return args.timeoutMs;
|
|
252
|
+
if (iosSdkCommands.has(command) || command === 'ios-execution' || require('./ios-wda-port').commands.has(command)) return 30000;
|
|
253
|
+
if (command === 'ios-setup') return 300000;
|
|
254
|
+
if (command === 'ios-install-app') return 120000;
|
|
255
|
+
if (command.startsWith('web-')) return 5000;
|
|
256
|
+
if (command.startsWith('ios-')) return undefined;
|
|
257
|
+
return command === 'logcat' && args.follow === true ? (args.durationSec ?? 5) * 1000 + 1000 : 30000;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// These are execution capabilities, independent of MCP/CLI transport. Script
|
|
261
|
+
// permissions gate Bridge calls; trusted local code is not a process sandbox.
|
|
262
|
+
const scriptPermissions = Object.freeze({
|
|
263
|
+
'app.read': ['status', 'tree', 'uia-tree', 'screenshot', 'flutter-tree', 'flutter-nodes', 'h5-dom', 'flutter-h5-dom', 'keyboard-state', 'permission-state',
|
|
264
|
+
'ios-status', 'ios-tree', 'ios-uia-tree', 'ios-screenshot', 'ios-flutter-tree', 'ios-flutter-nodes', 'ios-h5-dom', 'ios-wda-status', 'web-status', 'web-dom'],
|
|
265
|
+
'capture.read': ['logs', 'network', 'state', 'events', 'logcat', 'webview-console', 'webview-network', 'ios-logs', 'ios-network', 'ios-state', 'ios-events', 'web-logs', 'web-network', 'web-state', 'web-events'],
|
|
266
|
+
'app.interact': ['launch-app', 'launch-activity', 'tap', 'tap-text', 'tap-uia-text', 'tap-uia', 'tap-native', 'input-text', 'swipe', 'native-gesture', 'keyevent', 'wait-text', 'hide-keyboard', 'tap-flutter', 'tap-flutter-text', 'input-flutter-text', 'scroll-flutter', 'h5-click', 'h5-input', 'h5-wait', 'h5-scroll', 'flutter-h5-click', 'flutter-h5-input', 'flutter-h5-wait', 'flutter-h5-scroll',
|
|
267
|
+
'ios-h5-click', 'ios-h5-input', 'ios-h5-scroll', 'ios-launch-app', 'ios-wda-session', 'ios-tap', 'ios-input', 'ios-swipe', 'ios-set-orientation', 'ios-tap-native', 'ios-input-native-text', 'ios-tap-flutter', 'ios-input-flutter-text', 'ios-scroll-flutter', 'ios-flutter-back', 'ios-flutter-hide-keyboard', 'web-click', 'web-input', 'web-key', 'web-scroll', 'web-wait'],
|
|
268
|
+
'app.lifecycle': ['clear-app-data'],
|
|
269
|
+
'app.permissions': ['permission-grant', 'permission-revoke', 'appops-set'],
|
|
270
|
+
});
|
|
271
|
+
const scriptPermissionByCommand = new Map(Object.entries(scriptPermissions).flatMap(([permission, commands]) => commands.map(command => [command, permission])));
|
|
272
|
+
const workflowCommands = new Set(['install-apk', 'permission-dialog']);
|
|
273
|
+
const permissionCommands = new Set(['permission-state', 'permission-grant', 'permission-revoke']);
|
|
274
|
+
const expertCommands = new Set(['uia-runtime', 'freeze-app', 'thaw-app', 'flutter-action', 'h5-eval', 'flutter-h5-eval', 'ios-h5-eval', 'ios-flutter-action', 'ios-setup', 'web-command', 'forward', 'remove-forward', 'webview-pages', 'appops-set', 'permission-grant', 'permission-revoke']);
|
|
275
|
+
const deviceCommands = new Set(['uia-tree', 'screenshot', 'tap', 'tap-uia-text', 'keyboard-state', 'hide-keyboard', 'swipe', 'keyevent', 'logcat', 'permission-dialog', 'install-apk', 'remove-forward']);
|
|
276
|
+
function commandContract(command) {
|
|
277
|
+
const definition = commandByName.get(command) || isolatedByName.get(command);
|
|
278
|
+
if (!definition) throw new CommandError('unknown_command', `Unknown command: ${command}`, { field: 'command' });
|
|
279
|
+
const isolated = isolatedByName.has(command);
|
|
280
|
+
const platform = command === 'runtime' ? 'host' : isolated ? (command === 'evidence' ? 'host' : 'multi')
|
|
281
|
+
: definition.domain === 'ios' ? 'ios' : definition.domain === 'web' ? 'web' : 'android';
|
|
282
|
+
const role = command === 'runtime' || command === 'intent' || command === 'script' || workflowCommands.has(command) ? 'execution'
|
|
283
|
+
: command === 'evidence' ? 'evidence' : expertCommands.has(command) ? 'expert' : 'capability';
|
|
284
|
+
const permission = scriptPermissionByCommand.get(command) || null;
|
|
285
|
+
return {
|
|
286
|
+
role, platform,
|
|
287
|
+
targetKind: definition.targetKind || (isolated ? command === 'evidence' ? 'operation' : 'platform-target'
|
|
288
|
+
: definition.targetApp ? 'android-app' : deviceCommands.has(command) ? 'android-device' : 'host'),
|
|
289
|
+
entrypoints: { mcp: true, cli: true, script: permission !== null },
|
|
290
|
+
script: { supported: permission !== null, permission },
|
|
291
|
+
...(command === 'intent' ? { executablePlatforms: ['android', 'ios', 'web'],
|
|
292
|
+
providersByPlatform: { android: ['native', 'uia', 'flutter', 'h5'], ios: ['native', 'h5', 'flutter'], web: ['h5'] } } : {}),
|
|
293
|
+
execution: { kind: role === 'execution' ? 'operation' : isMutationCommand(command) ? 'mutation' : 'query',
|
|
294
|
+
mutation: isMutationCommand(command),
|
|
295
|
+
conditionalMutation: command === 'uia-runtime' ? 'operation != status' : command === 'logcat' ? 'clear=true' : ['webview-network', 'webview-console'].includes(command) ? 'script is supplied' : null,
|
|
296
|
+
arbitration: command === 'script' || command === 'intent' ? 'target-platform-physical-device'
|
|
297
|
+
: isAndroidMutation(command) ? 'cross-process-physical-android-device'
|
|
298
|
+
: platform === 'ios' && isMutationCommand(command) ? 'cross-process-physical-ios-device'
|
|
299
|
+
: command === 'device-ownership' ? 'exclusive-device-reconciliation' : null },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function commandSchema(command) {
|
|
304
|
+
const definition = commandByName.get(command) || isolatedByName.get(command);
|
|
305
|
+
if (!definition) throw new CommandError('unknown_command', `Unknown command: ${command}`, { field: 'command' });
|
|
306
|
+
if (command === 'runtime') return { type: 'object', additionalProperties: false, required: ['operation'],
|
|
307
|
+
properties: { operation: { enum: ['start', 'status', 'stop'] } } };
|
|
308
|
+
if (definition.domain === 'web') return require('./web/command-schema').webSchema(command);
|
|
309
|
+
if (command === 'device-ownership') return { type: 'object', additionalProperties: false,
|
|
310
|
+
properties: { operation: { enum: ['status', 'reconcile', 'receipt'] }, serial: { type: 'string', minLength: 1 },
|
|
311
|
+
runtimeEpoch: { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' },
|
|
312
|
+
actionId: { type: 'string', minLength: 1, maxLength: 1024 }, timeoutMs: { type: 'integer', minimum: 1, maximum: 30000 } },
|
|
313
|
+
required: ['operation', 'serial'], oneOf: [
|
|
314
|
+
{ properties: { operation: { enum: ['status', 'reconcile'] }, runtimeEpoch: false, actionId: false } },
|
|
315
|
+
{ properties: { operation: { const: 'receipt' }, timeoutMs: false }, required: ['runtimeEpoch', 'actionId'] },
|
|
316
|
+
] };
|
|
317
|
+
if (require('./ios-wda-port').commands.has(command)) {
|
|
318
|
+
const names = ['deviceId', 'wdaRunnerBundleId', 'wdaUrl', 'devicectl', 'timeoutMs', ...executionOptions];
|
|
319
|
+
if (command !== 'ios-wda-status') names.push('bundleId', 'wdaSessionId');
|
|
320
|
+
const properties = Object.fromEntries(names.map(name => [name, optionTypes[name]]));
|
|
321
|
+
const required = ['deviceId', 'wdaRunnerBundleId'];
|
|
322
|
+
if (command === 'ios-wda-session') {
|
|
323
|
+
properties.operation = { enum: ['status', 'create', 'close'] };
|
|
324
|
+
return { type: 'object', additionalProperties: false, properties, required: [...required, 'operation'], oneOf: [
|
|
325
|
+
{ properties: { operation: { const: 'status' }, bundleId: false, wdaSessionId: false } },
|
|
326
|
+
{ properties: { operation: { const: 'create' }, wdaSessionId: false }, required: ['bundleId'] },
|
|
327
|
+
{ properties: { operation: { const: 'close' } }, required: ['bundleId', 'wdaSessionId'] },
|
|
328
|
+
] };
|
|
329
|
+
}
|
|
330
|
+
if (command !== 'ios-wda-status') required.push('bundleId', 'wdaSessionId');
|
|
331
|
+
for (const name of definition.options.filter(name => !['bundleId','wdaUrl','wdaSessionId'].includes(name))) properties[name] = optionTypes[name];
|
|
332
|
+
required.push(...(requiredOptions[command] || []));
|
|
333
|
+
const schema = { type: 'object', additionalProperties: false, properties, required: [...new Set(required)] };
|
|
334
|
+
if (command === 'ios-tap-native' || command === 'ios-input-native-text') {
|
|
335
|
+
const native = require('./shared-kernel/ios-native-target');
|
|
336
|
+
properties.selector = native.selectorSchema;
|
|
337
|
+
properties.expectedTarget = native.expectedTargetSchema;
|
|
338
|
+
if (command === 'ios-input-native-text') properties.text = { type: 'string', maxLength: 16384 };
|
|
339
|
+
}
|
|
340
|
+
if (command === 'ios-input') {
|
|
341
|
+
schema.properties.text = { type: 'string', maxLength: 16384 };
|
|
342
|
+
schema.oneOf = [
|
|
343
|
+
{ required: ['elementId'], properties: { accessibilityId: false } },
|
|
344
|
+
{ required: ['accessibilityId'], properties: { elementId: false } },
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
return schema;
|
|
348
|
+
}
|
|
349
|
+
if (command === 'ios-execution') return { type: 'object', additionalProperties: false,
|
|
350
|
+
properties: { operation: { enum: ['status', 'result', 'cancel', 'reconcile'] },
|
|
351
|
+
deviceId: optionTypes.deviceId, bundleId: optionTypes.bundleId, kind: { enum: ['h5', 'flutter', 'wda'] },
|
|
352
|
+
actionId: { type: 'string', minLength: 1, maxLength: 256 }, runtimeEpoch: optionTypes.runtimeEpoch,
|
|
353
|
+
wdaRunnerBundleId: optionTypes.wdaRunnerBundleId, wdaUrl: optionTypes.wdaUrl,
|
|
354
|
+
runtimeUrl: optionTypes.runtimeUrl, iosHost: optionTypes.iosHost, iosPort: optionTypes.iosPort,
|
|
355
|
+
devicectl: optionTypes.devicectl, timeoutMs: optionTypes.timeoutMs },
|
|
356
|
+
required: ['operation', 'deviceId'], oneOf: [
|
|
357
|
+
{ properties: { kind: { enum: ['h5', 'flutter'] }, wdaRunnerBundleId: false, wdaUrl: false },
|
|
358
|
+
required: ['bundleId'], anyOf: [
|
|
359
|
+
{ properties: { operation: { enum: ['status', 'reconcile'] }, kind: false, actionId: false, runtimeEpoch: false } },
|
|
360
|
+
{ properties: { operation: { enum: ['result', 'cancel'] } }, required: ['kind', 'actionId', 'runtimeEpoch'] },
|
|
361
|
+
] },
|
|
362
|
+
{ properties: { kind: { const: 'wda' }, bundleId: false, runtimeUrl: false, iosHost: false, iosPort: false },
|
|
363
|
+
required: ['kind', 'wdaRunnerBundleId'], anyOf: [
|
|
364
|
+
{ properties: { operation: { enum: ['status', 'reconcile'] }, actionId: false, runtimeEpoch: false } },
|
|
365
|
+
{ properties: { operation: { enum: ['result', 'cancel'] } }, required: ['actionId', 'runtimeEpoch'] },
|
|
366
|
+
] },
|
|
367
|
+
] };
|
|
368
|
+
if (command === 'uia-runtime') return { type: 'object', additionalProperties: false,
|
|
369
|
+
properties: { operation: { enum: ['status', 'start', 'stop'] }, serial: { type: 'string', minLength: 1 },
|
|
370
|
+
adb: { type: 'string', minLength: 1 }, timeoutMs: { type: 'integer', minimum: 1, maximum: 30000 } }, required: ['operation', 'serial'] };
|
|
371
|
+
if (isolatedByName.has(command)) return executionCommandSchema(command, Object.keys(scriptPermissions));
|
|
372
|
+
const common = isolatedByName.has(command) || workflowCommands.has(command) || permissionCommands.has(command) ? [] : definition.domain === 'ios' ? iosCommon
|
|
373
|
+
: definition.domain === 'web' ? [] : androidCommon;
|
|
374
|
+
const names = new Set([...definition.options, ...common, ...(isolatedByName.has(command) || workflowCommands.has(command) ? [] : executionOptions), ...(extraOptions[command] || [])]);
|
|
375
|
+
if (historyCommands.has(command)) ['history', 'factCursor', 'cursor'].forEach(n => names.add(n));
|
|
376
|
+
if (captureCommands.has(command)) ['view', 'runtimeEpoch', 'afterActionId', 'mobileFactId', 'targetKey', 'timeoutMs'].forEach(n => names.add(n));
|
|
377
|
+
const properties = {};
|
|
378
|
+
for (const name of names) {
|
|
379
|
+
if (!optionTypes[name]) throw new Error(`Missing command option schema: ${command}.${name}`);
|
|
380
|
+
properties[name] = optionTypes[name];
|
|
381
|
+
}
|
|
382
|
+
if (['ios-h5-click', 'ios-h5-input', 'ios-h5-scroll'].includes(command)) {
|
|
383
|
+
const h5 = require('./shared-kernel/ios-h5-target');
|
|
384
|
+
properties.selector = h5.selectorSchema;
|
|
385
|
+
properties.expectedTarget = h5.expectedTargetSchema;
|
|
386
|
+
if (command === 'ios-h5-input') properties.text = { type: 'string', maxLength: 16384 };
|
|
387
|
+
}
|
|
388
|
+
if (['h5-click', 'h5-input', 'h5-scroll', 'h5-wait', 'h5-eval'].includes(command)) {
|
|
389
|
+
const h5 = require('./shared-kernel/android-h5-target');
|
|
390
|
+
if (properties.selector) properties.selector = h5.selectorSchema;
|
|
391
|
+
if (properties.expectedTarget) properties.expectedTarget = h5.expectedTargetSchema;
|
|
392
|
+
if (properties.expectedPage) properties.expectedPage = h5.pageSchema;
|
|
393
|
+
if (command === 'h5-input') properties.text = { type: 'string', maxLength: 16384 };
|
|
394
|
+
}
|
|
395
|
+
if (['flutter-h5-click', 'flutter-h5-input', 'flutter-h5-scroll', 'flutter-h5-wait', 'flutter-h5-eval'].includes(command)) {
|
|
396
|
+
const h5 = require('./shared-kernel/flutter-h5-target');
|
|
397
|
+
if (properties.selector) properties.selector = h5.selectorSchema;
|
|
398
|
+
if (properties.expectedTarget) properties.expectedTarget = h5.expectedTargetSchema;
|
|
399
|
+
if (properties.expectedPage) properties.expectedPage = h5.pageSchema;
|
|
400
|
+
if (command === 'flutter-h5-input') properties.text = { type: 'string', maxLength: 16384 };
|
|
401
|
+
}
|
|
402
|
+
if (command === 'tap-text') properties.provider = { enum: ['auto', 'native', 'flutter', 'uia'], default: 'auto' };
|
|
403
|
+
if (command === 'tap-uia-text') properties.exact = { type: 'boolean', enum: [true], default: true };
|
|
404
|
+
if (command === 'tap-uia') properties.selector = require('./shared-kernel/uia-target').selectorSchema;
|
|
405
|
+
if (command === 'tap-native' || command === 'input-text') properties.selector = nativeSelector;
|
|
406
|
+
if (command === 'input-text') properties.text = { type: 'string', maxLength: 16384 };
|
|
407
|
+
if (command === 'clear-app-data') properties.method = { enum: ['pm-clear', 'runtime'], default: 'pm-clear' };
|
|
408
|
+
if (command === 'tree' || command === 'uia-tree') {
|
|
409
|
+
properties.maxDepth = { type: 'integer', minimum: 0, maximum: 200 };
|
|
410
|
+
properties.maxNodes = { type: 'integer', minimum: 1, maximum: 1000 };
|
|
411
|
+
}
|
|
412
|
+
if (command === 'flutter-action' || command === 'ios-flutter-action') properties.payload = flutterActionSchema();
|
|
413
|
+
if (command === 'native-gesture') properties.payload = nativeGestureSchema();
|
|
414
|
+
if (['tap-flutter', 'input-flutter-text', 'scroll-flutter', 'ios-tap-flutter', 'ios-input-flutter-text', 'ios-scroll-flutter'].includes(command)) properties.selector = flutterSelector;
|
|
415
|
+
if (command === 'web-command') {
|
|
416
|
+
const { name, arguments: args, ...common } = properties;
|
|
417
|
+
return webCommandSchema(common);
|
|
418
|
+
}
|
|
419
|
+
const required = [...(requiredOptions[command] || [])];
|
|
420
|
+
if (iosSdkCommands.has(command)) required.push('deviceId', 'bundleId');
|
|
421
|
+
if (properties.port) properties.port.description = 'Host TCP forwarding port. The Android SDK endpoint is discovered from packageName.';
|
|
422
|
+
if (command === 'remove-forward') required.push('serial', 'port');
|
|
423
|
+
if (definition.targetApp && !required.includes('packageName')) required.push('packageName');
|
|
424
|
+
if (isAndroidMutation(command)) required.push('serial');
|
|
425
|
+
if (definition.targetKind === 'web-target') required.push('sessionId');
|
|
426
|
+
if (command === 'wait-text') {
|
|
427
|
+
properties.provider = { type: 'string', enum: ['auto', 'native', 'flutter', 'uia'], default: 'auto' };
|
|
428
|
+
properties.timeoutMs = { ...properties.timeoutMs, default: 10000 };
|
|
429
|
+
for (const name of ['requireText', 'absentText']) properties[name] = { type: 'array', maxItems: 64, items: { type: 'string', minLength: 1 } };
|
|
430
|
+
}
|
|
431
|
+
const schema = { type: 'object', additionalProperties: false, properties, required };
|
|
432
|
+
if (command === 'tap-uia') schema.oneOf = [
|
|
433
|
+
{ required: ['selector'], properties: { targetRef: false } },
|
|
434
|
+
{ required: ['targetRef'], properties: { selector: false } },
|
|
435
|
+
];
|
|
436
|
+
if (command === 'h5-scroll' || command === 'flutter-h5-scroll') schema.oneOf = [
|
|
437
|
+
{ required: ['selector'], properties: { expectedPage: false, deltaX: false, deltaY: false } },
|
|
438
|
+
{ required: ['deltaX', 'deltaY'], properties: { selector: false, expectedTarget: false },
|
|
439
|
+
not: { properties: { deltaX: { const: 0 }, deltaY: { const: 0 } } } },
|
|
440
|
+
];
|
|
441
|
+
if (command === 'tap-flutter') schema.oneOf = [
|
|
442
|
+
{ required: ['selector'], not: { anyOf: [{ required: ['tapX'] }, { required: ['tapY'] }] } },
|
|
443
|
+
{ required: ['tapX', 'tapY'], not: { required: ['selector'] } },
|
|
444
|
+
];
|
|
445
|
+
if (command === 'input-flutter-text' || command === 'input-text') schema.allOf = [
|
|
446
|
+
{ not: { required: ['selector'], anyOf: [{ required: ['tapX'] }, { required: ['tapY'] }] } },
|
|
447
|
+
];
|
|
448
|
+
return schema;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function invalid(field, message, details) {
|
|
452
|
+
return new CommandError('invalid_argument', message, { field, details });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function validateCommandArguments(command, args = {}) {
|
|
456
|
+
const schema = commandSchema(command);
|
|
457
|
+
validateValue(args, schema);
|
|
458
|
+
const normalized = { ...args };
|
|
459
|
+
const definition = commandByName.get(command);
|
|
460
|
+
if (command === 'wait-text') require('./shared-kernel/text-wait').validateTextConditions(normalized);
|
|
461
|
+
if (command === 'tap' && normalized.scope === 'app' && !normalized.packageName) throw new CommandError('target_required', 'App-scoped tap requires packageName.', { field: 'packageName' });
|
|
462
|
+
if (['input-text', 'input-flutter-text', 'ios-input'].includes(command)
|
|
463
|
+
&& Object.hasOwn(normalized, 'tapX') !== Object.hasOwn(normalized, 'tapY')) {
|
|
464
|
+
throw invalid('tapX', 'tapX and tapY must be supplied together, or both omitted.');
|
|
465
|
+
}
|
|
466
|
+
if (['web-wait'].includes(command)
|
|
467
|
+
&& Boolean(normalized.selector) === Boolean(normalized.targetText)) {
|
|
468
|
+
throw invalid('selector', 'Supply exactly one of selector or targetText.');
|
|
469
|
+
}
|
|
470
|
+
if (isMutationCommand(command, normalized) && normalized.history === true) throw invalid('history', 'A mutation cannot be requested as a history read.');
|
|
471
|
+
if (isAndroidMutation(command, normalized) && !normalized.serial) throw new CommandError('missing_argument', `${command}: serial is required for device mutation.`, { field: 'serial' });
|
|
472
|
+
return normalized;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// CLI parsing is the only place that converts a textual number/boolean. MCP
|
|
476
|
+
// and programmatic calls always validate the original JSON types.
|
|
477
|
+
function parseCliOptions(command, options) {
|
|
478
|
+
const schema = commandSchema(command);
|
|
479
|
+
const parsed = { ...options };
|
|
480
|
+
for (const [name, value] of Object.entries(options)) {
|
|
481
|
+
const property = schema.properties[name];
|
|
482
|
+
if (!property || typeof value !== 'string') continue;
|
|
483
|
+
if (property.type === 'array' && ['category', 'extra'].includes(name)) {
|
|
484
|
+
parsed[name] = [value];
|
|
485
|
+
} else if (jsonCliValue(property)) {
|
|
486
|
+
try { parsed[name] = JSON.parse(value); }
|
|
487
|
+
catch { throw invalid(name, `${name} must be valid JSON matching its input schema.`); }
|
|
488
|
+
} else if (property.type === 'integer' || property.type === 'number') {
|
|
489
|
+
if (!/^-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/.test(value.trim())) throw invalid(name, `${name} must be a number.`);
|
|
490
|
+
parsed[name] = Number(value);
|
|
491
|
+
} else if (property.type === 'boolean' && (value === 'true' || value === 'false')) {
|
|
492
|
+
parsed[name] = value === 'true';
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return parsed;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function jsonCliValue(schema) {
|
|
499
|
+
return ['object', 'array'].includes(schema.type) || schema.contentMediaType === 'application/json'
|
|
500
|
+
|| [...(schema.anyOf || []), ...(schema.oneOf || [])].some(jsonCliValue);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
module.exports = { commandDefinitions, isolatedCommandDefinitions, commandByName, isolatedByName,
|
|
504
|
+
commandSchema, commandContract, scriptPermissions, scriptPermissionByCommand, validateCommandArguments, parseCliOptions, isMutationCommand, isAndroidMutation, executionTimeoutMs };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CommandError } = require('./command-errors');
|
|
4
|
+
const { validateCommandArguments } = require('./command-registry');
|
|
5
|
+
|
|
6
|
+
function validateRunRequest(args) {
|
|
7
|
+
if (!args || typeof args !== 'object' || Array.isArray(args)) throw new CommandError('invalid_argument', 'run requires an object.', { field: 'arguments' });
|
|
8
|
+
const extra = Object.keys(args).find(key => key !== 'command' && key !== 'arguments');
|
|
9
|
+
if (extra) throw new CommandError('unsupported_argument', 'Put all command parameters in run.arguments.', { field: extra });
|
|
10
|
+
const command = typeof args.command === 'string' ? args.command : '';
|
|
11
|
+
return { command, arguments: validateCommandArguments(command, args.arguments === undefined ? {} : args.arguments) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { validateRunRequest };
|