agent-device 0.10.1 → 0.10.3
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/README.md +4 -1
- package/dist/src/376.js +3 -0
- package/dist/src/bin.js +74 -73
- package/dist/src/daemon.js +39 -39
- package/dist/src/index.d.ts +559 -5
- package/dist/src/index.js +3 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +8 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +60 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +1 -1
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +4 -0
- package/macos-helper/Package.swift +18 -0
- package/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +543 -0
- package/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +545 -0
- package/package.json +5 -1
- package/skills/agent-device/SKILL.md +57 -343
- package/skills/agent-device/references/bootstrap-install.md +207 -0
- package/skills/agent-device/references/coordinate-system.md +24 -4
- package/skills/agent-device/references/debugging.md +115 -0
- package/skills/agent-device/references/exploration.md +235 -0
- package/skills/agent-device/references/macos-desktop.md +55 -58
- package/skills/agent-device/references/remote-tenancy.md +69 -47
- package/skills/agent-device/references/verification.md +102 -0
- package/dist/src/224.js +0 -2
- package/dist/src/274.js +0 -1
- package/dist/src/331.js +0 -3
- package/dist/src/bin.d.ts +0 -1
- package/dist/src/cli-client-commands.d.ts +0 -8
- package/dist/src/cli.d.ts +0 -6
- package/dist/src/client-metro.d.ts +0 -64
- package/dist/src/client-normalizers.d.ts +0 -20
- package/dist/src/client-shared.d.ts +0 -20
- package/dist/src/client-types.d.ts +0 -269
- package/dist/src/client.d.ts +0 -5
- package/dist/src/core/app-events.d.ts +0 -8
- package/dist/src/core/batch.d.ts +0 -17
- package/dist/src/core/capabilities.d.ts +0 -3
- package/dist/src/core/click-button.d.ts +0 -20
- package/dist/src/core/dispatch-payload.d.ts +0 -1
- package/dist/src/core/dispatch-resolve.d.ts +0 -29
- package/dist/src/core/dispatch-series.d.ts +0 -7
- package/dist/src/core/dispatch.d.ts +0 -35
- package/dist/src/core/open-target.d.ts +0 -4
- package/dist/src/core/settings-contract.d.ts +0 -8
- package/dist/src/daemon/action-utils.d.ts +0 -3
- package/dist/src/daemon/android-system-dialog.d.ts +0 -11
- package/dist/src/daemon/app-log-android.d.ts +0 -4
- package/dist/src/daemon/app-log-ios.d.ts +0 -6
- package/dist/src/daemon/app-log-process.d.ts +0 -15
- package/dist/src/daemon/app-log-stream.d.ts +0 -19
- package/dist/src/daemon/app-log.d.ts +0 -28
- package/dist/src/daemon/artifact-archive.d.ts +0 -12
- package/dist/src/daemon/artifact-download.d.ts +0 -12
- package/dist/src/daemon/artifact-materialization.d.ts +0 -17
- package/dist/src/daemon/artifact-registry.d.ts +0 -12
- package/dist/src/daemon/config.d.ts +0 -16
- package/dist/src/daemon/context.d.ts +0 -23
- package/dist/src/daemon/device-ready.d.ts +0 -6
- package/dist/src/daemon/handlers/find.d.ts +0 -40
- package/dist/src/daemon/handlers/install-source.d.ts +0 -44
- package/dist/src/daemon/handlers/interaction-common.d.ts +0 -12
- package/dist/src/daemon/handlers/interaction-fill.d.ts +0 -3
- package/dist/src/daemon/handlers/interaction-flags.d.ts +0 -4
- package/dist/src/daemon/handlers/interaction-get.d.ts +0 -3
- package/dist/src/daemon/handlers/interaction-is.d.ts +0 -3
- package/dist/src/daemon/handlers/interaction-press.d.ts +0 -3
- package/dist/src/daemon/handlers/interaction-scroll.d.ts +0 -3
- package/dist/src/daemon/handlers/interaction-selector.d.ts +0 -27
- package/dist/src/daemon/handlers/interaction-snapshot.d.ts +0 -8
- package/dist/src/daemon/handlers/interaction-targeting.d.ts +0 -28
- package/dist/src/daemon/handlers/interaction-touch.d.ts +0 -46
- package/dist/src/daemon/handlers/interaction.d.ts +0 -16
- package/dist/src/daemon/handlers/lease.d.ts +0 -8
- package/dist/src/daemon/handlers/parse-utils.d.ts +0 -3
- package/dist/src/daemon/handlers/record-trace-android.d.ts +0 -18
- package/dist/src/daemon/handlers/record-trace-ios.d.ts +0 -52
- package/dist/src/daemon/handlers/record-trace-recording.d.ts +0 -32
- package/dist/src/daemon/handlers/record-trace.d.ts +0 -10
- package/dist/src/daemon/handlers/session-batch.d.ts +0 -2
- package/dist/src/daemon/handlers/session-close.d.ts +0 -31
- package/dist/src/daemon/handlers/session-deploy.d.ts +0 -37
- package/dist/src/daemon/handlers/session-device-utils.d.ts +0 -26
- package/dist/src/daemon/handlers/session-open-target.d.ts +0 -3
- package/dist/src/daemon/handlers/session-open.d.ts +0 -22
- package/dist/src/daemon/handlers/session-perf.d.ts +0 -2
- package/dist/src/daemon/handlers/session-replay-heal.d.ts +0 -8
- package/dist/src/daemon/handlers/session-replay-script.d.ts +0 -3
- package/dist/src/daemon/handlers/session-runtime-command.d.ts +0 -9
- package/dist/src/daemon/handlers/session-runtime.d.ts +0 -36
- package/dist/src/daemon/handlers/session-startup-metrics.d.ts +0 -11
- package/dist/src/daemon/handlers/session.d.ts +0 -50
- package/dist/src/daemon/handlers/snapshot-alert.d.ts +0 -13
- package/dist/src/daemon/handlers/snapshot-capture.d.ts +0 -27
- package/dist/src/daemon/handlers/snapshot-session.d.ts +0 -15
- package/dist/src/daemon/handlers/snapshot-settings.d.ts +0 -24
- package/dist/src/daemon/handlers/snapshot-wait.d.ts +0 -37
- package/dist/src/daemon/handlers/snapshot.d.ts +0 -16
- package/dist/src/daemon/http-server.d.ts +0 -26
- package/dist/src/daemon/install-source-resolution.d.ts +0 -5
- package/dist/src/daemon/is-predicates.d.ts +0 -15
- package/dist/src/daemon/lease-context.d.ts +0 -9
- package/dist/src/daemon/lease-registry.d.ts +0 -63
- package/dist/src/daemon/materialized-path-registry.d.ts +0 -15
- package/dist/src/daemon/network-log.d.ts +0 -32
- package/dist/src/daemon/record-trace-errors.d.ts +0 -6
- package/dist/src/daemon/recording-gestures.d.ts +0 -3
- package/dist/src/daemon/recording-telemetry.d.ts +0 -20
- package/dist/src/daemon/recording-timing.d.ts +0 -24
- package/dist/src/daemon/request-cancel.d.ts +0 -9
- package/dist/src/daemon/request-lock-policy.d.ts +0 -2
- package/dist/src/daemon/request-router.d.ts +0 -23
- package/dist/src/daemon/runtime-hints.d.ts +0 -19
- package/dist/src/daemon/script-utils.d.ts +0 -28
- package/dist/src/daemon/scroll-planner.d.ts +0 -12
- package/dist/src/daemon/selectors-build.d.ts +0 -5
- package/dist/src/daemon/selectors-match.d.ts +0 -6
- package/dist/src/daemon/selectors-parse.d.ts +0 -29
- package/dist/src/daemon/selectors-resolve.d.ts +0 -33
- package/dist/src/daemon/selectors.d.ts +0 -5
- package/dist/src/daemon/server-lifecycle.d.ts +0 -23
- package/dist/src/daemon/session-open-script.d.ts +0 -7
- package/dist/src/daemon/session-routing.d.ts +0 -3
- package/dist/src/daemon/session-selector.d.ts +0 -10
- package/dist/src/daemon/session-store.d.ts +0 -33
- package/dist/src/daemon/snapshot-diff.d.ts +0 -20
- package/dist/src/daemon/snapshot-processing.d.ts +0 -9
- package/dist/src/daemon/touch-reference-frame.d.ts +0 -7
- package/dist/src/daemon/transport.d.ts +0 -6
- package/dist/src/daemon/types.d.ts +0 -171
- package/dist/src/daemon/upload-registry.d.ts +0 -7
- package/dist/src/daemon/upload.d.ts +0 -5
- package/dist/src/daemon-client.d.ts +0 -40
- package/dist/src/daemon.d.ts +0 -1
- package/dist/src/platforms/android/adb.d.ts +0 -5
- package/dist/src/platforms/android/app-lifecycle.d.ts +0 -31
- package/dist/src/platforms/android/device-input-state.d.ts +0 -19
- package/dist/src/platforms/android/devices.d.ts +0 -26
- package/dist/src/platforms/android/index.d.ts +0 -8
- package/dist/src/platforms/android/input-actions.d.ts +0 -16
- package/dist/src/platforms/android/install-artifact.d.ts +0 -11
- package/dist/src/platforms/android/manifest.d.ts +0 -1
- package/dist/src/platforms/android/notifications.d.ts +0 -11
- package/dist/src/platforms/android/open-target.d.ts +0 -4
- package/dist/src/platforms/android/screenshot.d.ts +0 -16
- package/dist/src/platforms/android/sdk.d.ts +0 -2
- package/dist/src/platforms/android/settings.d.ts +0 -3
- package/dist/src/platforms/android/snapshot.d.ts +0 -7
- package/dist/src/platforms/android/ui-hierarchy.d.ts +0 -21
- package/dist/src/platforms/appearance.d.ts +0 -2
- package/dist/src/platforms/boot-diagnostics.d.ts +0 -14
- package/dist/src/platforms/install-source.d.ts +0 -29
- package/dist/src/platforms/ios/app-filter.d.ts +0 -2
- package/dist/src/platforms/ios/apps.d.ts +0 -34
- package/dist/src/platforms/ios/config.d.ts +0 -10
- package/dist/src/platforms/ios/devicectl.d.ts +0 -13
- package/dist/src/platforms/ios/devices.d.ts +0 -40
- package/dist/src/platforms/ios/ensure-simulator.d.ts +0 -18
- package/dist/src/platforms/ios/index.d.ts +0 -3
- package/dist/src/platforms/ios/install-artifact.d.ts +0 -18
- package/dist/src/platforms/ios/launch-diagnostics.d.ts +0 -11
- package/dist/src/platforms/ios/macos-apps.d.ts +0 -12
- package/dist/src/platforms/ios/plist.d.ts +0 -1
- package/dist/src/platforms/ios/runner-client.d.ts +0 -38
- package/dist/src/platforms/ios/runner-errors.d.ts +0 -20
- package/dist/src/platforms/ios/runner-macos-products.d.ts +0 -3
- package/dist/src/platforms/ios/runner-session.d.ts +0 -30
- package/dist/src/platforms/ios/runner-transport.d.ts +0 -10
- package/dist/src/platforms/ios/runner-xctestrun-products.d.ts +0 -2
- package/dist/src/platforms/ios/runner-xctestrun.d.ts +0 -38
- package/dist/src/platforms/ios/screenshot-status-bar.d.ts +0 -2
- package/dist/src/platforms/ios/screenshot.d.ts +0 -14
- package/dist/src/platforms/ios/simctl.d.ts +0 -7
- package/dist/src/platforms/ios/simulator.d.ts +0 -11
- package/dist/src/platforms/permission-utils.d.ts +0 -9
- package/dist/src/recording/overlay.d.ts +0 -10
- package/dist/src/upload-client.d.ts +0 -7
- package/dist/src/utils/args.d.ts +0 -27
- package/dist/src/utils/cli-config.d.ts +0 -10
- package/dist/src/utils/cli-option-schema.d.ts +0 -19
- package/dist/src/utils/cli-options.d.ts +0 -13
- package/dist/src/utils/command-schema.d.ts +0 -122
- package/dist/src/utils/device-isolation.d.ts +0 -3
- package/dist/src/utils/device.d.ts +0 -35
- package/dist/src/utils/diagnostics.d.ts +0 -30
- package/dist/src/utils/errors.d.ts +0 -26
- package/dist/src/utils/exec.d.ts +0 -32
- package/dist/src/utils/finders.d.ts +0 -12
- package/dist/src/utils/interactors.d.ts +0 -38
- package/dist/src/utils/json-input.d.ts +0 -1
- package/dist/src/utils/keyed-lock.d.ts +0 -1
- package/dist/src/utils/output.d.ts +0 -27
- package/dist/src/utils/path-resolution.d.ts +0 -8
- package/dist/src/utils/payload-input.d.ts +0 -12
- package/dist/src/utils/process-identity.d.ts +0 -11
- package/dist/src/utils/remote-config.d.ts +0 -15
- package/dist/src/utils/remote-open.d.ts +0 -9
- package/dist/src/utils/retry.d.ts +0 -54
- package/dist/src/utils/screenshot-diff.d.ts +0 -23
- package/dist/src/utils/session-binding.d.ts +0 -18
- package/dist/src/utils/snapshot-lines.d.ts +0 -12
- package/dist/src/utils/snapshot.d.ts +0 -42
- package/dist/src/utils/timeouts.d.ts +0 -3
- package/dist/src/utils/version.d.ts +0 -2
- package/dist/src/utils/video.d.ts +0 -9
- package/skills/agent-device/references/batching.md +0 -79
- package/skills/agent-device/references/logs-and-debug.md +0 -113
- package/skills/agent-device/references/perf-metrics.md +0 -53
- package/skills/agent-device/references/permissions.md +0 -70
- package/skills/agent-device/references/session-management.md +0 -101
- package/skills/agent-device/references/snapshot-refs.md +0 -102
- package/skills/agent-device/references/video-recording.md +0 -49
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import ApplicationServices
|
|
3
|
+
import CoreGraphics
|
|
4
|
+
import Foundation
|
|
5
|
+
|
|
6
|
+
enum HelperError: Error {
|
|
7
|
+
case invalidArgs(String)
|
|
8
|
+
case commandFailed(String, details: [String: String] = [:])
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct ErrorPayload: Encodable {
|
|
12
|
+
let message: String
|
|
13
|
+
let details: [String: String]?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
struct SuccessEnvelope<T: Encodable>: Encodable {
|
|
17
|
+
let ok = true
|
|
18
|
+
let data: T
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
struct FailureEnvelope: Encodable {
|
|
22
|
+
let ok = false
|
|
23
|
+
let error: ErrorPayload
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
struct FrontmostAppResponse: Encodable {
|
|
27
|
+
let bundleId: String?
|
|
28
|
+
let appName: String?
|
|
29
|
+
let pid: Int32?
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
struct QuitAppResponse: Encodable {
|
|
33
|
+
let bundleId: String
|
|
34
|
+
let running: Bool
|
|
35
|
+
let terminated: Bool
|
|
36
|
+
let forceTerminated: Bool
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct PermissionResponse: Encodable {
|
|
40
|
+
let target: String
|
|
41
|
+
let action: String
|
|
42
|
+
let granted: Bool
|
|
43
|
+
let requested: Bool
|
|
44
|
+
let openedSettings: Bool
|
|
45
|
+
let message: String?
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
struct AlertResponse: Encodable {
|
|
49
|
+
let title: String?
|
|
50
|
+
let role: String?
|
|
51
|
+
let buttons: [String]
|
|
52
|
+
let action: String?
|
|
53
|
+
let bundleId: String?
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
struct ReadResponse: Encodable {
|
|
57
|
+
let text: String
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
struct AgentDeviceMacOSHelper {
|
|
61
|
+
static func main() {
|
|
62
|
+
do {
|
|
63
|
+
let output = try run(arguments: Array(CommandLine.arguments.dropFirst()))
|
|
64
|
+
try writeJSON(output)
|
|
65
|
+
Foundation.exit(0)
|
|
66
|
+
} catch let error as HelperError {
|
|
67
|
+
let payload: FailureEnvelope
|
|
68
|
+
switch error {
|
|
69
|
+
case .invalidArgs(let message):
|
|
70
|
+
payload = FailureEnvelope(error: ErrorPayload(message: message, details: nil))
|
|
71
|
+
case .commandFailed(let message, let details):
|
|
72
|
+
payload = FailureEnvelope(
|
|
73
|
+
error: ErrorPayload(message: message, details: details.isEmpty ? nil : details)
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
try? writeJSON(payload)
|
|
77
|
+
Foundation.exit(1)
|
|
78
|
+
} catch {
|
|
79
|
+
let payload = FailureEnvelope(
|
|
80
|
+
error: ErrorPayload(message: String(describing: error), details: nil)
|
|
81
|
+
)
|
|
82
|
+
try? writeJSON(payload)
|
|
83
|
+
Foundation.exit(1)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static func run(arguments: [String]) throws -> any Encodable {
|
|
88
|
+
guard let command = arguments.first else {
|
|
89
|
+
throw HelperError.invalidArgs("missing command")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
switch command {
|
|
93
|
+
case "app":
|
|
94
|
+
return try handleApp(arguments: Array(arguments.dropFirst()))
|
|
95
|
+
case "permission":
|
|
96
|
+
return try handlePermission(arguments: Array(arguments.dropFirst()))
|
|
97
|
+
case "alert":
|
|
98
|
+
return try handleAlert(arguments: Array(arguments.dropFirst()))
|
|
99
|
+
case "snapshot":
|
|
100
|
+
return try handleSnapshot(arguments: Array(arguments.dropFirst()))
|
|
101
|
+
case "read":
|
|
102
|
+
return try handleRead(arguments: Array(arguments.dropFirst()))
|
|
103
|
+
default:
|
|
104
|
+
throw HelperError.invalidArgs("unknown command: \(command)")
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static func handleApp(arguments: [String]) throws -> any Encodable {
|
|
109
|
+
guard let action = arguments.first else {
|
|
110
|
+
throw HelperError.invalidArgs("app requires frontmost|quit")
|
|
111
|
+
}
|
|
112
|
+
switch action {
|
|
113
|
+
case "frontmost":
|
|
114
|
+
let app = NSWorkspace.shared.frontmostApplication
|
|
115
|
+
return SuccessEnvelope(
|
|
116
|
+
data: FrontmostAppResponse(
|
|
117
|
+
bundleId: app?.bundleIdentifier,
|
|
118
|
+
appName: app?.localizedName,
|
|
119
|
+
pid: app.map { Int32($0.processIdentifier) }
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
case "quit":
|
|
123
|
+
guard let rawBundleId = optionValue(arguments: Array(arguments.dropFirst()), name: "--bundle-id"),
|
|
124
|
+
!rawBundleId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
125
|
+
else {
|
|
126
|
+
throw HelperError.invalidArgs("app quit requires --bundle-id <id>")
|
|
127
|
+
}
|
|
128
|
+
let bundleId = try validatedBundleId(rawBundleId)
|
|
129
|
+
let apps = NSRunningApplication.runningApplications(withBundleIdentifier: bundleId)
|
|
130
|
+
guard let app = apps.first else {
|
|
131
|
+
return SuccessEnvelope(
|
|
132
|
+
data: QuitAppResponse(
|
|
133
|
+
bundleId: bundleId,
|
|
134
|
+
running: false,
|
|
135
|
+
terminated: false,
|
|
136
|
+
forceTerminated: false
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
let terminated = app.terminate()
|
|
141
|
+
if terminated {
|
|
142
|
+
return SuccessEnvelope(
|
|
143
|
+
data: QuitAppResponse(
|
|
144
|
+
bundleId: bundleId,
|
|
145
|
+
running: true,
|
|
146
|
+
terminated: true,
|
|
147
|
+
forceTerminated: false
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
let forceTerminated = app.forceTerminate()
|
|
152
|
+
return SuccessEnvelope(
|
|
153
|
+
data: QuitAppResponse(
|
|
154
|
+
bundleId: bundleId,
|
|
155
|
+
running: true,
|
|
156
|
+
terminated: false,
|
|
157
|
+
forceTerminated: forceTerminated
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
default:
|
|
161
|
+
throw HelperError.invalidArgs("app requires frontmost|quit")
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
static func handlePermission(arguments: [String]) throws -> any Encodable {
|
|
166
|
+
guard arguments.count >= 2 else {
|
|
167
|
+
throw HelperError.invalidArgs(
|
|
168
|
+
"permission requires <grant|reset> <accessibility|screen-recording|input-monitoring>"
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
let action = arguments[0]
|
|
172
|
+
let target = arguments[1]
|
|
173
|
+
switch (action, target) {
|
|
174
|
+
case ("grant", "accessibility"):
|
|
175
|
+
let before = AXIsProcessTrusted()
|
|
176
|
+
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true]
|
|
177
|
+
let after = AXIsProcessTrustedWithOptions(options as CFDictionary)
|
|
178
|
+
return SuccessEnvelope(
|
|
179
|
+
data: PermissionResponse(
|
|
180
|
+
target: target,
|
|
181
|
+
action: action,
|
|
182
|
+
granted: before || after,
|
|
183
|
+
requested: !before,
|
|
184
|
+
openedSettings: false,
|
|
185
|
+
message: before ? "Accessibility access already granted." : "Requested Accessibility access."
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
case ("reset", "accessibility"):
|
|
189
|
+
let opened = openPrivacyPane(anchor: "Privacy_Accessibility")
|
|
190
|
+
return SuccessEnvelope(
|
|
191
|
+
data: PermissionResponse(
|
|
192
|
+
target: target,
|
|
193
|
+
action: action,
|
|
194
|
+
granted: AXIsProcessTrusted(),
|
|
195
|
+
requested: false,
|
|
196
|
+
openedSettings: opened,
|
|
197
|
+
message: "macOS requires Accessibility access to be changed manually in System Settings."
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
case ("grant", "screen-recording"):
|
|
201
|
+
let before = CGPreflightScreenCaptureAccess()
|
|
202
|
+
let requested = !before
|
|
203
|
+
let after = before || CGRequestScreenCaptureAccess()
|
|
204
|
+
return SuccessEnvelope(
|
|
205
|
+
data: PermissionResponse(
|
|
206
|
+
target: target,
|
|
207
|
+
action: action,
|
|
208
|
+
granted: after,
|
|
209
|
+
requested: requested,
|
|
210
|
+
openedSettings: requested && !after ? openPrivacyPane(anchor: "Privacy_ScreenCapture") : false,
|
|
211
|
+
message: after ? "Screen Recording access is available." : "Grant Screen Recording access in System Settings."
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
case ("reset", "screen-recording"):
|
|
215
|
+
let opened = openPrivacyPane(anchor: "Privacy_ScreenCapture")
|
|
216
|
+
return SuccessEnvelope(
|
|
217
|
+
data: PermissionResponse(
|
|
218
|
+
target: target,
|
|
219
|
+
action: action,
|
|
220
|
+
granted: CGPreflightScreenCaptureAccess(),
|
|
221
|
+
requested: false,
|
|
222
|
+
openedSettings: opened,
|
|
223
|
+
message: "macOS requires Screen Recording access to be changed manually in System Settings."
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
case ("grant", "input-monitoring"):
|
|
227
|
+
let before = CGPreflightPostEventAccess()
|
|
228
|
+
let requested = !before
|
|
229
|
+
let after = before || CGRequestPostEventAccess()
|
|
230
|
+
return SuccessEnvelope(
|
|
231
|
+
data: PermissionResponse(
|
|
232
|
+
target: target,
|
|
233
|
+
action: action,
|
|
234
|
+
granted: after,
|
|
235
|
+
requested: requested,
|
|
236
|
+
openedSettings: requested && !after ? openPrivacyPane(anchor: "Privacy_ListenEvent") : false,
|
|
237
|
+
message: after ? "Input Monitoring access is available." : "Grant Input Monitoring access in System Settings."
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
case ("reset", "input-monitoring"):
|
|
241
|
+
let opened = openPrivacyPane(anchor: "Privacy_ListenEvent")
|
|
242
|
+
return SuccessEnvelope(
|
|
243
|
+
data: PermissionResponse(
|
|
244
|
+
target: target,
|
|
245
|
+
action: action,
|
|
246
|
+
granted: CGPreflightPostEventAccess(),
|
|
247
|
+
requested: false,
|
|
248
|
+
openedSettings: opened,
|
|
249
|
+
message: "macOS requires Input Monitoring access to be changed manually in System Settings."
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
default:
|
|
253
|
+
throw HelperError.invalidArgs(
|
|
254
|
+
"permission requires <grant|reset> <accessibility|screen-recording|input-monitoring>"
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
static func handleAlert(arguments: [String]) throws -> any Encodable {
|
|
260
|
+
let action = arguments.first ?? "get"
|
|
261
|
+
guard action == "get" || action == "accept" || action == "dismiss" else {
|
|
262
|
+
throw HelperError.invalidArgs("alert requires get|accept|dismiss")
|
|
263
|
+
}
|
|
264
|
+
let bundleId = optionValue(arguments: Array(arguments.dropFirst()), name: "--bundle-id")
|
|
265
|
+
let surface = optionValue(arguments: Array(arguments.dropFirst()), name: "--surface")
|
|
266
|
+
let app = try resolveTargetApplication(bundleId: bundleId, surface: surface)
|
|
267
|
+
guard let alertElement = findAlertElement(appElement: AXUIElementCreateApplication(app.processIdentifier)) else {
|
|
268
|
+
throw HelperError.commandFailed(
|
|
269
|
+
"alert not found",
|
|
270
|
+
details: ["bundleId": app.bundleIdentifier ?? "", "appName": app.localizedName ?? ""]
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
let buttons = collectButtons(root: alertElement)
|
|
274
|
+
let labels = buttons.map(resolveElementLabel)
|
|
275
|
+
let role = stringAttribute(alertElement, attribute: kAXRoleAttribute as String)
|
|
276
|
+
let title =
|
|
277
|
+
stringAttribute(alertElement, attribute: kAXTitleAttribute as String)
|
|
278
|
+
?? stringAttribute(alertElement, attribute: kAXDescriptionAttribute as String)
|
|
279
|
+
|
|
280
|
+
if action == "accept" || action == "dismiss" {
|
|
281
|
+
guard let button = resolveAlertActionButton(
|
|
282
|
+
root: alertElement,
|
|
283
|
+
buttons: buttons,
|
|
284
|
+
action: action
|
|
285
|
+
)
|
|
286
|
+
else {
|
|
287
|
+
throw HelperError.commandFailed("alert action failed", details: ["reason": "missing_button"])
|
|
288
|
+
}
|
|
289
|
+
let status = AXUIElementPerformAction(button, kAXPressAction as CFString)
|
|
290
|
+
guard status == .success else {
|
|
291
|
+
throw HelperError.commandFailed(
|
|
292
|
+
"alert action failed",
|
|
293
|
+
details: ["status": "\(status.rawValue)"]
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return SuccessEnvelope(
|
|
299
|
+
data: AlertResponse(
|
|
300
|
+
title: title,
|
|
301
|
+
role: role,
|
|
302
|
+
buttons: labels,
|
|
303
|
+
action: action == "get" ? nil : action,
|
|
304
|
+
bundleId: app.bundleIdentifier
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static func handleSnapshot(arguments: [String]) throws -> any Encodable {
|
|
310
|
+
guard let surface = optionValue(arguments: arguments, name: "--surface")?
|
|
311
|
+
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
312
|
+
.lowercased(),
|
|
313
|
+
!surface.isEmpty
|
|
314
|
+
else {
|
|
315
|
+
throw HelperError.invalidArgs("snapshot requires --surface <frontmost-app|desktop|menubar>")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
switch surface {
|
|
319
|
+
case "frontmost-app":
|
|
320
|
+
return SuccessEnvelope(data: try captureSnapshotResponse(surface: surface))
|
|
321
|
+
case "desktop", "menubar":
|
|
322
|
+
return SuccessEnvelope(data: try captureSnapshotResponse(surface: surface))
|
|
323
|
+
default:
|
|
324
|
+
throw HelperError.invalidArgs("snapshot requires --surface <frontmost-app|desktop|menubar>")
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static func handleRead(arguments: [String]) throws -> any Encodable {
|
|
329
|
+
guard let rawX = optionValue(arguments: arguments, name: "--x"),
|
|
330
|
+
let rawY = optionValue(arguments: arguments, name: "--y"),
|
|
331
|
+
let x = Double(rawX),
|
|
332
|
+
let y = Double(rawY)
|
|
333
|
+
else {
|
|
334
|
+
throw HelperError.invalidArgs("read requires --x <number> --y <number>")
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let bundleId = optionValue(arguments: arguments, name: "--bundle-id")
|
|
338
|
+
let surface = optionValue(arguments: arguments, name: "--surface")
|
|
339
|
+
let text = try readTextAtPosition(bundleId: bundleId, surface: surface, x: x, y: y)
|
|
340
|
+
return SuccessEnvelope(data: ReadResponse(text: text))
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private func optionValue(arguments: [String], name: String) -> String? {
|
|
345
|
+
guard let index = arguments.firstIndex(of: name), arguments.indices.contains(index + 1) else {
|
|
346
|
+
return nil
|
|
347
|
+
}
|
|
348
|
+
return arguments[index + 1]
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private func readTextAtPosition(bundleId: String?, surface: String?, x: Double, y: Double) throws -> String {
|
|
352
|
+
let targetApp: NSRunningApplication?
|
|
353
|
+
if surface == "frontmost-app" || (surface == nil && bundleId != nil) {
|
|
354
|
+
targetApp = try resolveTargetApplication(bundleId: bundleId, surface: surface)
|
|
355
|
+
} else {
|
|
356
|
+
targetApp = nil
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let systemWide = AXUIElementCreateSystemWide()
|
|
360
|
+
var hitElement: AXUIElement?
|
|
361
|
+
guard AXUIElementCopyElementAtPosition(systemWide, Float(x), Float(y), &hitElement) == .success,
|
|
362
|
+
let hitElement
|
|
363
|
+
else {
|
|
364
|
+
throw HelperError.commandFailed("read did not resolve an accessibility element")
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if let targetApp {
|
|
368
|
+
var pid: pid_t = 0
|
|
369
|
+
guard AXUIElementGetPid(hitElement, &pid) == .success else {
|
|
370
|
+
throw HelperError.commandFailed("read could not resolve element owner")
|
|
371
|
+
}
|
|
372
|
+
guard pid == targetApp.processIdentifier else {
|
|
373
|
+
throw HelperError.commandFailed(
|
|
374
|
+
"read resolved text from a different app",
|
|
375
|
+
details: [
|
|
376
|
+
"expectedPid": String(targetApp.processIdentifier),
|
|
377
|
+
"actualPid": String(pid)
|
|
378
|
+
]
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var current: AXUIElement? = hitElement
|
|
384
|
+
while let element = current {
|
|
385
|
+
if let text = readableText(for: element) {
|
|
386
|
+
return text
|
|
387
|
+
}
|
|
388
|
+
let parent = elementAttribute(element, attribute: kAXParentAttribute as String)
|
|
389
|
+
if let parent, CFEqual(parent, element) {
|
|
390
|
+
break
|
|
391
|
+
}
|
|
392
|
+
current = parent
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
throw HelperError.commandFailed("read did not resolve text")
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private func readableText(for element: AXUIElement) -> String? {
|
|
399
|
+
return
|
|
400
|
+
stringAttribute(element, attribute: kAXValueAttribute as String)
|
|
401
|
+
?? stringAttribute(element, attribute: kAXTitleAttribute as String)
|
|
402
|
+
?? stringAttribute(element, attribute: kAXDescriptionAttribute as String)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private func openPrivacyPane(anchor: String) -> Bool {
|
|
406
|
+
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?\(anchor)") {
|
|
407
|
+
if NSWorkspace.shared.open(url) {
|
|
408
|
+
return true
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if let appUrl = URL(string: "file:///System/Applications/System%20Settings.app") {
|
|
412
|
+
return NSWorkspace.shared.open(appUrl)
|
|
413
|
+
}
|
|
414
|
+
return false
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private func writeJSON<T: Encodable>(_ value: T) throws {
|
|
418
|
+
let encoder = JSONEncoder()
|
|
419
|
+
encoder.outputFormatting = [.sortedKeys]
|
|
420
|
+
let data = try encoder.encode(value)
|
|
421
|
+
FileHandle.standardOutput.write(data)
|
|
422
|
+
FileHandle.standardOutput.write(Data([0x0A]))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
func resolveTargetApplication(bundleId: String?, surface: String?) throws -> NSRunningApplication {
|
|
426
|
+
let normalizedSurface = surface?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
427
|
+
if normalizedSurface == "desktop" || normalizedSurface == "menubar" {
|
|
428
|
+
throw HelperError.commandFailed(
|
|
429
|
+
"alert surface is not supported yet",
|
|
430
|
+
details: ["surface": normalizedSurface ?? ""]
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
if normalizedSurface == "frontmost-app" {
|
|
434
|
+
if let frontmost = NSWorkspace.shared.frontmostApplication {
|
|
435
|
+
return frontmost
|
|
436
|
+
}
|
|
437
|
+
throw HelperError.commandFailed("unable to resolve frontmost app")
|
|
438
|
+
}
|
|
439
|
+
if let bundleId, !bundleId.isEmpty {
|
|
440
|
+
let validatedBundleId = try validatedBundleId(bundleId)
|
|
441
|
+
if let app = NSRunningApplication.runningApplications(withBundleIdentifier: validatedBundleId).first {
|
|
442
|
+
return app
|
|
443
|
+
}
|
|
444
|
+
throw HelperError.commandFailed("app is not running", details: ["bundleId": validatedBundleId])
|
|
445
|
+
}
|
|
446
|
+
if let frontmost = NSWorkspace.shared.frontmostApplication {
|
|
447
|
+
return frontmost
|
|
448
|
+
}
|
|
449
|
+
throw HelperError.commandFailed("unable to resolve target app")
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private func validatedBundleId(_ rawBundleId: String) throws -> String {
|
|
453
|
+
let bundleId = rawBundleId.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
454
|
+
let pattern = #"^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$"#
|
|
455
|
+
guard bundleId.range(of: pattern, options: .regularExpression) != nil else {
|
|
456
|
+
throw HelperError.invalidArgs("bundle id must use reverse-DNS form like com.example.App")
|
|
457
|
+
}
|
|
458
|
+
return bundleId
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private func findAlertElement(appElement: AXUIElement) -> AXUIElement? {
|
|
462
|
+
for window in windows(of: appElement) {
|
|
463
|
+
if let role = stringAttribute(window, attribute: kAXRoleAttribute as String),
|
|
464
|
+
role == "AXSheet" || role == "AXDialog"
|
|
465
|
+
{
|
|
466
|
+
return window
|
|
467
|
+
}
|
|
468
|
+
if let nested = findAlertElementRecursively(root: window, depth: 0) {
|
|
469
|
+
return nested
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return nil
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private func findAlertElementRecursively(root: AXUIElement, depth: Int) -> AXUIElement? {
|
|
476
|
+
if depth > 4 {
|
|
477
|
+
return nil
|
|
478
|
+
}
|
|
479
|
+
for child in children(of: root) {
|
|
480
|
+
if let role = stringAttribute(child, attribute: kAXRoleAttribute as String),
|
|
481
|
+
role == "AXSheet" || role == "AXDialog"
|
|
482
|
+
{
|
|
483
|
+
return child
|
|
484
|
+
}
|
|
485
|
+
if let nested = findAlertElementRecursively(root: child, depth: depth + 1) {
|
|
486
|
+
return nested
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return nil
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private func collectButtons(root: AXUIElement) -> [AXUIElement] {
|
|
493
|
+
var buttons: [AXUIElement] = []
|
|
494
|
+
collectButtons(root: root, depth: 0, results: &buttons)
|
|
495
|
+
return buttons
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private func collectButtons(root: AXUIElement, depth: Int, results: inout [AXUIElement]) {
|
|
499
|
+
if depth > 5 {
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
if stringAttribute(root, attribute: kAXRoleAttribute as String) == "AXButton" {
|
|
503
|
+
results.append(root)
|
|
504
|
+
}
|
|
505
|
+
for child in children(of: root) {
|
|
506
|
+
collectButtons(root: child, depth: depth + 1, results: &results)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private func resolveElementLabel(_ element: AXUIElement) -> String {
|
|
511
|
+
return
|
|
512
|
+
stringAttribute(element, attribute: kAXTitleAttribute as String)
|
|
513
|
+
?? stringAttribute(element, attribute: kAXDescriptionAttribute as String)
|
|
514
|
+
?? stringAttribute(element, attribute: kAXValueAttribute as String)
|
|
515
|
+
?? "button"
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private func resolveAlertActionButton(root: AXUIElement, buttons: [AXUIElement], action: String) -> AXUIElement? {
|
|
519
|
+
if action == "accept",
|
|
520
|
+
let defaultButton = elementAttribute(root, attribute: kAXDefaultButtonAttribute as String)
|
|
521
|
+
{
|
|
522
|
+
return defaultButton
|
|
523
|
+
}
|
|
524
|
+
if action == "dismiss",
|
|
525
|
+
let cancelButton = elementAttribute(root, attribute: kAXCancelButtonAttribute as String)
|
|
526
|
+
{
|
|
527
|
+
return cancelButton
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
let buttonEntries = buttons.map { (element: $0, label: resolveElementLabel($0).lowercased()) }
|
|
531
|
+
let preferredLabels =
|
|
532
|
+
action == "accept"
|
|
533
|
+
? ["allow", "ok", "open", "continue", "yes", "save", "install", "trust", "enable"]
|
|
534
|
+
: ["don't allow", "deny", "cancel", "not now", "no", "close", "later", "ignore"]
|
|
535
|
+
|
|
536
|
+
for preferredLabel in preferredLabels {
|
|
537
|
+
if let match = buttonEntries.first(where: { $0.label.contains(preferredLabel) }) {
|
|
538
|
+
return match.element
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return action == "accept" ? buttons.first : buttons.last
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
AgentDeviceMacOSHelper.main()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-device",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Callstack",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"build:xcuitest:ios": "rm -rf ~/.agent-device/ios-runner/derived/device && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=iOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived",
|
|
28
28
|
"build:xcuitest:macos": "rm -rf ~/.agent-device/ios-runner/derived/macos && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"platform=macOS,arch=$(uname -m)\" -derivedDataPath ~/.agent-device/ios-runner/derived/macos CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY=\"\" DEVELOPMENT_TEAM=\"\" COMPILER_INDEX_STORE_ENABLE=NO ENABLE_CODE_COVERAGE=NO",
|
|
29
29
|
"build:xcuitest:tvos": "rm -rf ~/.agent-device/ios-runner/derived/tvos && xcodebuild build-for-testing -project ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj -scheme AgentDeviceRunner -destination \"generic/platform=tvOS Simulator\" -derivedDataPath ~/.agent-device/ios-runner/derived/tvos",
|
|
30
|
+
"build:macos-helper": "swift build -c release --package-path macos-helper",
|
|
30
31
|
"build:all": "pnpm build:node && pnpm build:xcuitest",
|
|
31
32
|
"ad": "node bin/agent-device.mjs",
|
|
32
33
|
"format": "prettier --write src test skills",
|
|
@@ -46,6 +47,8 @@
|
|
|
46
47
|
"!ios-runner/**/.swiftpm",
|
|
47
48
|
"!ios-runner/**/xcuserdata",
|
|
48
49
|
"!ios-runner/**/*.xcuserstate",
|
|
50
|
+
"macos-helper",
|
|
51
|
+
"!macos-helper/**/.build",
|
|
49
52
|
"skills",
|
|
50
53
|
"README.md",
|
|
51
54
|
"LICENSE"
|
|
@@ -70,6 +73,7 @@
|
|
|
70
73
|
"pngjs": "^7.0.0"
|
|
71
74
|
},
|
|
72
75
|
"devDependencies": {
|
|
76
|
+
"@microsoft/api-extractor": "^7.52.10",
|
|
73
77
|
"@rslib/core": "0.19.4",
|
|
74
78
|
"@types/node": "^22.0.0",
|
|
75
79
|
"@types/pngjs": "^6.0.5",
|