agent-device 0.11.1 → 0.11.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/dist/src/1~rslib-runtime.js +1 -0
- package/dist/src/916.js +3 -0
- package/dist/src/bin.js +56 -63
- package/dist/src/daemon.js +41 -40
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -3
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +14 -3
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +10 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +1 -0
- package/macos-helper/Sources/AgentDeviceMacOSHelper/SnapshotTraversal.swift +235 -34
- package/macos-helper/Sources/AgentDeviceMacOSHelper/main.swift +121 -2
- package/package.json +11 -11
- package/skills/agent-device/references/macos-desktop.md +3 -2
- package/dist/src/306.js +0 -3
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import AppKit
|
|
2
2
|
import ApplicationServices
|
|
3
|
+
import CoreGraphics
|
|
3
4
|
import Foundation
|
|
4
5
|
|
|
5
6
|
private enum SnapshotTraversalLimits {
|
|
6
7
|
static let maxDesktopApps = 24
|
|
7
8
|
static let maxNodes = 1500
|
|
8
9
|
static let maxDepth = 12
|
|
10
|
+
static let maxMenuBarBandY = 64.0
|
|
11
|
+
static let maxMenuBarBandHeight = 64.0
|
|
12
|
+
static let maxMenuBarExtraWidth = 256.0
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
struct RectResponse: Encodable {
|
|
@@ -62,7 +66,17 @@ private struct SnapshotTraversalState {
|
|
|
62
66
|
var truncated = false
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
private struct MenuBarWindowFallbackCandidate {
|
|
70
|
+
let windowNumber: Int
|
|
71
|
+
let rect: RectResponse
|
|
72
|
+
let layer: Int
|
|
73
|
+
|
|
74
|
+
var area: Double {
|
|
75
|
+
rect.width * rect.height
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func captureSnapshotResponse(surface: String, bundleId: String? = nil) throws -> SnapshotResponse {
|
|
66
80
|
let result: SnapshotBuildResult
|
|
67
81
|
switch surface {
|
|
68
82
|
case "frontmost-app":
|
|
@@ -70,7 +84,7 @@ func captureSnapshotResponse(surface: String) throws -> SnapshotResponse {
|
|
|
70
84
|
case "desktop":
|
|
71
85
|
result = snapshotDesktop()
|
|
72
86
|
case "menubar":
|
|
73
|
-
result = snapshotMenuBar()
|
|
87
|
+
result = try snapshotMenuBar(bundleId: bundleId)
|
|
74
88
|
default:
|
|
75
89
|
throw HelperError.invalidArgs("snapshot requires --surface <frontmost-app|desktop|menubar>")
|
|
76
90
|
}
|
|
@@ -200,8 +214,9 @@ private func appendApplicationSnapshot(
|
|
|
200
214
|
return true
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
private func snapshotMenuBar() -> SnapshotBuildResult {
|
|
217
|
+
private func snapshotMenuBar(bundleId: String?) throws -> SnapshotBuildResult {
|
|
204
218
|
var state = SnapshotTraversalState()
|
|
219
|
+
let screenRect = mainScreenRectResponse()
|
|
205
220
|
guard
|
|
206
221
|
let rootIndex = appendSyntheticSnapshotNode(
|
|
207
222
|
into: &state,
|
|
@@ -209,29 +224,54 @@ private func snapshotMenuBar() -> SnapshotBuildResult {
|
|
|
209
224
|
label: "Menu Bar",
|
|
210
225
|
depth: 0,
|
|
211
226
|
parentIndex: nil,
|
|
212
|
-
surface: "menubar"
|
|
227
|
+
surface: "menubar",
|
|
228
|
+
rect: screenRect
|
|
213
229
|
)
|
|
214
230
|
else {
|
|
215
231
|
return SnapshotBuildResult(nodes: state.nodes, truncated: true)
|
|
216
232
|
}
|
|
217
233
|
|
|
218
|
-
if let
|
|
219
|
-
let
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
if let bundleId {
|
|
235
|
+
let targetApp = try resolveTargetApplication(bundleId: bundleId, surface: nil)
|
|
236
|
+
let appendedExtras = appendMenuBarSnapshot(
|
|
237
|
+
targetApp,
|
|
238
|
+
attribute: kAXExtrasMenuBarAttribute as String,
|
|
239
|
+
depth: 1,
|
|
240
|
+
parentIndex: rootIndex,
|
|
241
|
+
surface: "menubar",
|
|
242
|
+
state: &state
|
|
243
|
+
)
|
|
244
|
+
if !appendedExtras {
|
|
245
|
+
let appendedMenuBar = appendMenuBarSnapshot(
|
|
246
|
+
targetApp,
|
|
247
|
+
attribute: kAXMenuBarAttribute as String,
|
|
223
248
|
depth: 1,
|
|
224
249
|
parentIndex: rootIndex,
|
|
225
|
-
|
|
226
|
-
surface: "menubar",
|
|
227
|
-
pid: Int32(frontmost.processIdentifier),
|
|
228
|
-
bundleId: frontmost.bundleIdentifier,
|
|
229
|
-
appName: frontmost.localizedName,
|
|
230
|
-
windowTitle: frontmost.localizedName
|
|
231
|
-
),
|
|
250
|
+
surface: "menubar",
|
|
232
251
|
state: &state
|
|
233
252
|
)
|
|
253
|
+
if !appendedMenuBar {
|
|
254
|
+
_ = appendMenuBarWindowFallback(
|
|
255
|
+
targetApp,
|
|
256
|
+
depth: 1,
|
|
257
|
+
parentIndex: rootIndex,
|
|
258
|
+
surface: "menubar",
|
|
259
|
+
state: &state
|
|
260
|
+
)
|
|
261
|
+
}
|
|
234
262
|
}
|
|
263
|
+
return SnapshotBuildResult(nodes: state.nodes, truncated: state.truncated)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if let frontmost = NSWorkspace.shared.frontmostApplication {
|
|
267
|
+
_ = appendMenuBarSnapshot(
|
|
268
|
+
frontmost,
|
|
269
|
+
attribute: kAXMenuBarAttribute as String,
|
|
270
|
+
depth: 1,
|
|
271
|
+
parentIndex: rootIndex,
|
|
272
|
+
surface: "menubar",
|
|
273
|
+
state: &state
|
|
274
|
+
)
|
|
235
275
|
}
|
|
236
276
|
|
|
237
277
|
if !state.truncated,
|
|
@@ -239,20 +279,24 @@ private func snapshotMenuBar() -> SnapshotBuildResult {
|
|
|
239
279
|
withBundleIdentifier: "com.apple.systemuiserver"
|
|
240
280
|
).first
|
|
241
281
|
{
|
|
242
|
-
let
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
282
|
+
let appendedExtras = appendMenuBarSnapshot(
|
|
283
|
+
systemUiServer,
|
|
284
|
+
attribute: kAXExtrasMenuBarAttribute as String,
|
|
285
|
+
depth: 1,
|
|
286
|
+
parentIndex: rootIndex,
|
|
287
|
+
surface: "menubar",
|
|
288
|
+
state: &state,
|
|
289
|
+
windowTitle: "System Menu Extras"
|
|
290
|
+
)
|
|
291
|
+
if !appendedExtras {
|
|
292
|
+
_ = appendMenuBarSnapshot(
|
|
293
|
+
systemUiServer,
|
|
294
|
+
attribute: kAXMenuBarAttribute as String,
|
|
246
295
|
depth: 1,
|
|
247
296
|
parentIndex: rootIndex,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
bundleId: systemUiServer.bundleIdentifier,
|
|
252
|
-
appName: systemUiServer.localizedName,
|
|
253
|
-
windowTitle: "System Menu Extras"
|
|
254
|
-
),
|
|
255
|
-
state: &state
|
|
297
|
+
surface: "menubar",
|
|
298
|
+
state: &state,
|
|
299
|
+
windowTitle: "System Menu Extras"
|
|
256
300
|
)
|
|
257
301
|
}
|
|
258
302
|
}
|
|
@@ -260,6 +304,67 @@ private func snapshotMenuBar() -> SnapshotBuildResult {
|
|
|
260
304
|
return SnapshotBuildResult(nodes: state.nodes, truncated: state.truncated)
|
|
261
305
|
}
|
|
262
306
|
|
|
307
|
+
@discardableResult
|
|
308
|
+
private func appendMenuBarSnapshot(
|
|
309
|
+
_ app: NSRunningApplication,
|
|
310
|
+
attribute: String,
|
|
311
|
+
depth: Int,
|
|
312
|
+
parentIndex: Int,
|
|
313
|
+
surface: String,
|
|
314
|
+
state: inout SnapshotTraversalState,
|
|
315
|
+
windowTitle: String? = nil
|
|
316
|
+
) -> Bool {
|
|
317
|
+
let appElement = AXUIElementCreateApplication(app.processIdentifier)
|
|
318
|
+
guard let menuBar = elementAttribute(appElement, attribute: attribute) else {
|
|
319
|
+
return false
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let nodeCountBefore = state.nodes.count
|
|
323
|
+
_ = appendElementSnapshot(
|
|
324
|
+
menuBar,
|
|
325
|
+
depth: depth,
|
|
326
|
+
parentIndex: parentIndex,
|
|
327
|
+
context: SnapshotContext(
|
|
328
|
+
surface: surface,
|
|
329
|
+
pid: Int32(app.processIdentifier),
|
|
330
|
+
bundleId: app.bundleIdentifier,
|
|
331
|
+
appName: app.localizedName,
|
|
332
|
+
windowTitle: windowTitle ?? app.localizedName
|
|
333
|
+
),
|
|
334
|
+
state: &state
|
|
335
|
+
)
|
|
336
|
+
return state.nodes.count > nodeCountBefore
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@discardableResult
|
|
340
|
+
private func appendMenuBarWindowFallback(
|
|
341
|
+
_ app: NSRunningApplication,
|
|
342
|
+
depth: Int,
|
|
343
|
+
parentIndex: Int,
|
|
344
|
+
surface: String,
|
|
345
|
+
state: inout SnapshotTraversalState
|
|
346
|
+
) -> Bool {
|
|
347
|
+
guard let candidate = menuBarWindowFallbackCandidate(for: app) else {
|
|
348
|
+
return false
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return appendSyntheticSnapshotNode(
|
|
352
|
+
into: &state,
|
|
353
|
+
type: "MenuBarItem",
|
|
354
|
+
label: app.localizedName ?? app.bundleIdentifier ?? "Menu Bar Item",
|
|
355
|
+
depth: depth,
|
|
356
|
+
parentIndex: parentIndex,
|
|
357
|
+
surface: surface,
|
|
358
|
+
identifier: "cgwindow:\(candidate.windowNumber)",
|
|
359
|
+
pid: Int32(app.processIdentifier),
|
|
360
|
+
bundleId: app.bundleIdentifier,
|
|
361
|
+
appName: app.localizedName,
|
|
362
|
+
windowTitle: app.localizedName,
|
|
363
|
+
rect: candidate.rect,
|
|
364
|
+
hittable: true
|
|
365
|
+
) != nil
|
|
366
|
+
}
|
|
367
|
+
|
|
263
368
|
@discardableResult
|
|
264
369
|
private func appendSyntheticSnapshotNode(
|
|
265
370
|
into state: inout SnapshotTraversalState,
|
|
@@ -272,7 +377,9 @@ private func appendSyntheticSnapshotNode(
|
|
|
272
377
|
pid: Int32? = nil,
|
|
273
378
|
bundleId: String? = nil,
|
|
274
379
|
appName: String? = nil,
|
|
275
|
-
windowTitle: String? = nil
|
|
380
|
+
windowTitle: String? = nil,
|
|
381
|
+
rect: RectResponse? = nil,
|
|
382
|
+
hittable: Bool = false
|
|
276
383
|
) -> Int? {
|
|
277
384
|
guard reserveSnapshotNodeCapacity(&state) else {
|
|
278
385
|
return nil
|
|
@@ -288,10 +395,10 @@ private func appendSyntheticSnapshotNode(
|
|
|
288
395
|
label: label,
|
|
289
396
|
value: nil,
|
|
290
397
|
identifier: identifier ?? "surface:\(surface):\(type.lowercased())",
|
|
291
|
-
rect:
|
|
398
|
+
rect: rect,
|
|
292
399
|
enabled: true,
|
|
293
400
|
selected: nil,
|
|
294
|
-
hittable:
|
|
401
|
+
hittable: hittable && rect != nil,
|
|
295
402
|
depth: depth,
|
|
296
403
|
parentIndex: parentIndex,
|
|
297
404
|
pid: pid,
|
|
@@ -304,6 +411,83 @@ private func appendSyntheticSnapshotNode(
|
|
|
304
411
|
return index
|
|
305
412
|
}
|
|
306
413
|
|
|
414
|
+
private func mainScreenRectResponse() -> RectResponse? {
|
|
415
|
+
guard let screenFrame = NSScreen.main?.frame, screenFrame.width > 0, screenFrame.height > 0 else {
|
|
416
|
+
return nil
|
|
417
|
+
}
|
|
418
|
+
return RectResponse(
|
|
419
|
+
x: Double(screenFrame.origin.x),
|
|
420
|
+
y: Double(screenFrame.origin.y),
|
|
421
|
+
width: Double(screenFrame.width),
|
|
422
|
+
height: Double(screenFrame.height)
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private func menuBarWindowFallbackCandidate(
|
|
427
|
+
for app: NSRunningApplication
|
|
428
|
+
) -> MenuBarWindowFallbackCandidate? {
|
|
429
|
+
guard
|
|
430
|
+
let windowInfoList = CGWindowListCopyWindowInfo([.optionAll], kCGNullWindowID)
|
|
431
|
+
as? [[String: Any]]
|
|
432
|
+
else {
|
|
433
|
+
return nil
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let pid = Int(app.processIdentifier)
|
|
437
|
+
let allCandidates = windowInfoList.compactMap { info -> MenuBarWindowFallbackCandidate? in
|
|
438
|
+
let ownerPid = (info[kCGWindowOwnerPID as String] as? NSNumber)?.intValue
|
|
439
|
+
guard ownerPid == pid else {
|
|
440
|
+
return nil
|
|
441
|
+
}
|
|
442
|
+
guard let boundsDictionary = info[kCGWindowBounds as String] as? NSDictionary,
|
|
443
|
+
let bounds = CGRect(dictionaryRepresentation: boundsDictionary)
|
|
444
|
+
else {
|
|
445
|
+
return nil
|
|
446
|
+
}
|
|
447
|
+
guard bounds.width > 0, bounds.height > 0 else {
|
|
448
|
+
return nil
|
|
449
|
+
}
|
|
450
|
+
let alpha = (info[kCGWindowAlpha as String] as? NSNumber)?.doubleValue ?? 1
|
|
451
|
+
guard alpha > 0 else {
|
|
452
|
+
return nil
|
|
453
|
+
}
|
|
454
|
+
let rect = RectResponse(
|
|
455
|
+
x: Double(bounds.origin.x),
|
|
456
|
+
y: Double(bounds.origin.y),
|
|
457
|
+
width: Double(bounds.width),
|
|
458
|
+
height: Double(bounds.height)
|
|
459
|
+
)
|
|
460
|
+
let windowNumber = (info[kCGWindowNumber as String] as? NSNumber)?.intValue ?? 0
|
|
461
|
+
let layer = (info[kCGWindowLayer as String] as? NSNumber)?.intValue ?? 0
|
|
462
|
+
return MenuBarWindowFallbackCandidate(
|
|
463
|
+
windowNumber: windowNumber,
|
|
464
|
+
rect: rect,
|
|
465
|
+
layer: layer
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// CGWindowList can surface multiple app-owned utility windows. Prefer the small
|
|
470
|
+
// top-band window that matches typical menu bar extra geometry before ranking by area.
|
|
471
|
+
let menuBarBandCandidates = allCandidates.filter { candidate in
|
|
472
|
+
candidate.rect.y <= SnapshotTraversalLimits.maxMenuBarBandY
|
|
473
|
+
&& candidate.rect.height <= SnapshotTraversalLimits.maxMenuBarBandHeight
|
|
474
|
+
}
|
|
475
|
+
let narrowCandidates = menuBarBandCandidates.filter { candidate in
|
|
476
|
+
candidate.rect.width <= SnapshotTraversalLimits.maxMenuBarExtraWidth
|
|
477
|
+
}
|
|
478
|
+
let rankedCandidates = (narrowCandidates.isEmpty ? menuBarBandCandidates : narrowCandidates)
|
|
479
|
+
.sorted { left, right in
|
|
480
|
+
if left.area != right.area {
|
|
481
|
+
return left.area < right.area
|
|
482
|
+
}
|
|
483
|
+
if left.layer != right.layer {
|
|
484
|
+
return left.layer < right.layer
|
|
485
|
+
}
|
|
486
|
+
return left.windowNumber < right.windowNumber
|
|
487
|
+
}
|
|
488
|
+
return rankedCandidates.first
|
|
489
|
+
}
|
|
490
|
+
|
|
307
491
|
@discardableResult
|
|
308
492
|
private func appendElementSnapshot(
|
|
309
493
|
_ element: AXUIElement,
|
|
@@ -361,7 +545,7 @@ private func appendElementSnapshot(
|
|
|
361
545
|
return index
|
|
362
546
|
}
|
|
363
547
|
|
|
364
|
-
for child in
|
|
548
|
+
for child in snapshotChildren(of: element, role: role) {
|
|
365
549
|
if state.truncated {
|
|
366
550
|
break
|
|
367
551
|
}
|
|
@@ -522,9 +706,9 @@ private func accessibilityAxValue(_ value: CFTypeRef?) -> AXValue? {
|
|
|
522
706
|
return (value as! AXValue)
|
|
523
707
|
}
|
|
524
708
|
|
|
525
|
-
func
|
|
709
|
+
private func elementArrayAttribute(_ element: AXUIElement, attribute: String) -> [AXUIElement] {
|
|
526
710
|
var value: CFTypeRef?
|
|
527
|
-
guard AXUIElementCopyAttributeValue(element,
|
|
711
|
+
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
|
|
528
712
|
let children = value as? [AXUIElement]
|
|
529
713
|
else {
|
|
530
714
|
return []
|
|
@@ -532,6 +716,23 @@ func children(of element: AXUIElement) -> [AXUIElement] {
|
|
|
532
716
|
return children
|
|
533
717
|
}
|
|
534
718
|
|
|
719
|
+
func children(of element: AXUIElement) -> [AXUIElement] {
|
|
720
|
+
return elementArrayAttribute(element, attribute: kAXChildrenAttribute as String)
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private func snapshotChildren(of element: AXUIElement, role: String?) -> [AXUIElement] {
|
|
724
|
+
let directChildren = children(of: element)
|
|
725
|
+
if !directChildren.isEmpty {
|
|
726
|
+
return directChildren
|
|
727
|
+
}
|
|
728
|
+
switch role {
|
|
729
|
+
case "AXMenuBar", "AXMenuBarItem", "AXMenu":
|
|
730
|
+
return elementArrayAttribute(element, attribute: kAXVisibleChildrenAttribute as String)
|
|
731
|
+
default:
|
|
732
|
+
return []
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
535
736
|
func windows(of appElement: AXUIElement) -> [AXUIElement] {
|
|
536
737
|
var value: CFTypeRef?
|
|
537
738
|
guard AXUIElementCopyAttributeValue(appElement, "AXWindows" as CFString, &value) == .success,
|
|
@@ -2,6 +2,9 @@ import AppKit
|
|
|
2
2
|
import ApplicationServices
|
|
3
3
|
import CoreGraphics
|
|
4
4
|
import Foundation
|
|
5
|
+
import ImageIO
|
|
6
|
+
import ScreenCaptureKit
|
|
7
|
+
import UniformTypeIdentifiers
|
|
5
8
|
|
|
6
9
|
enum HelperError: Error {
|
|
7
10
|
case invalidArgs(String)
|
|
@@ -57,6 +60,19 @@ struct ReadResponse: Encodable {
|
|
|
57
60
|
let text: String
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
struct PressResponse: Encodable {
|
|
64
|
+
let x: Double
|
|
65
|
+
let y: Double
|
|
66
|
+
let bundleId: String?
|
|
67
|
+
let surface: String?
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
struct ScreenshotResponse: Encodable {
|
|
71
|
+
let path: String
|
|
72
|
+
let surface: String?
|
|
73
|
+
let fullscreen: Bool
|
|
74
|
+
}
|
|
75
|
+
|
|
60
76
|
struct AgentDeviceMacOSHelper {
|
|
61
77
|
static func main() {
|
|
62
78
|
do {
|
|
@@ -100,6 +116,10 @@ struct AgentDeviceMacOSHelper {
|
|
|
100
116
|
return try handleSnapshot(arguments: Array(arguments.dropFirst()))
|
|
101
117
|
case "read":
|
|
102
118
|
return try handleRead(arguments: Array(arguments.dropFirst()))
|
|
119
|
+
case "press":
|
|
120
|
+
return try handlePress(arguments: Array(arguments.dropFirst()))
|
|
121
|
+
case "screenshot":
|
|
122
|
+
return try handleScreenshot(arguments: Array(arguments.dropFirst()))
|
|
103
123
|
default:
|
|
104
124
|
throw HelperError.invalidArgs("unknown command: \(command)")
|
|
105
125
|
}
|
|
@@ -315,11 +335,13 @@ struct AgentDeviceMacOSHelper {
|
|
|
315
335
|
throw HelperError.invalidArgs("snapshot requires --surface <frontmost-app|desktop|menubar>")
|
|
316
336
|
}
|
|
317
337
|
|
|
338
|
+
let bundleId = try optionValue(arguments: arguments, name: "--bundle-id").map(validatedBundleId)
|
|
339
|
+
|
|
318
340
|
switch surface {
|
|
319
341
|
case "frontmost-app":
|
|
320
|
-
return SuccessEnvelope(data: try captureSnapshotResponse(surface: surface))
|
|
342
|
+
return SuccessEnvelope(data: try captureSnapshotResponse(surface: surface, bundleId: bundleId))
|
|
321
343
|
case "desktop", "menubar":
|
|
322
|
-
return SuccessEnvelope(data: try captureSnapshotResponse(surface: surface))
|
|
344
|
+
return SuccessEnvelope(data: try captureSnapshotResponse(surface: surface, bundleId: bundleId))
|
|
323
345
|
default:
|
|
324
346
|
throw HelperError.invalidArgs("snapshot requires --surface <frontmost-app|desktop|menubar>")
|
|
325
347
|
}
|
|
@@ -339,6 +361,35 @@ struct AgentDeviceMacOSHelper {
|
|
|
339
361
|
let text = try readTextAtPosition(bundleId: bundleId, surface: surface, x: x, y: y)
|
|
340
362
|
return SuccessEnvelope(data: ReadResponse(text: text))
|
|
341
363
|
}
|
|
364
|
+
|
|
365
|
+
static func handlePress(arguments: [String]) throws -> any Encodable {
|
|
366
|
+
guard let rawX = optionValue(arguments: arguments, name: "--x"),
|
|
367
|
+
let rawY = optionValue(arguments: arguments, name: "--y"),
|
|
368
|
+
let x = Double(rawX),
|
|
369
|
+
let y = Double(rawY)
|
|
370
|
+
else {
|
|
371
|
+
throw HelperError.invalidArgs("press requires --x <number> --y <number>")
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let bundleId = try optionValue(arguments: arguments, name: "--bundle-id").map(validatedBundleId)
|
|
375
|
+
let surface = optionValue(arguments: arguments, name: "--surface")
|
|
376
|
+
try pressAtPosition(bundleId: bundleId, surface: surface, x: x, y: y)
|
|
377
|
+
return SuccessEnvelope(data: PressResponse(x: x, y: y, bundleId: bundleId, surface: surface))
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
static func handleScreenshot(arguments: [String]) throws -> any Encodable {
|
|
381
|
+
guard let outPath = optionValue(arguments: arguments, name: "--out")?
|
|
382
|
+
.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
383
|
+
!outPath.isEmpty
|
|
384
|
+
else {
|
|
385
|
+
throw HelperError.invalidArgs("screenshot requires --out <path>")
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let surface = optionValue(arguments: arguments, name: "--surface")
|
|
389
|
+
let fullscreen = arguments.contains("--fullscreen")
|
|
390
|
+
try captureSurfaceScreenshot(surface: surface, outPath: outPath, fullscreen: fullscreen)
|
|
391
|
+
return SuccessEnvelope(data: ScreenshotResponse(path: outPath, surface: surface, fullscreen: fullscreen))
|
|
392
|
+
}
|
|
342
393
|
}
|
|
343
394
|
|
|
344
395
|
private func optionValue(arguments: [String], name: String) -> String? {
|
|
@@ -395,6 +446,74 @@ private func readTextAtPosition(bundleId: String?, surface: String?, x: Double,
|
|
|
395
446
|
throw HelperError.commandFailed("read did not resolve text")
|
|
396
447
|
}
|
|
397
448
|
|
|
449
|
+
private func pressAtPosition(bundleId: String?, surface: String?, x: Double, y: Double) throws {
|
|
450
|
+
_ = bundleId
|
|
451
|
+
_ = surface
|
|
452
|
+
let point = CGPoint(x: x, y: y)
|
|
453
|
+
guard let move = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left),
|
|
454
|
+
let down = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left),
|
|
455
|
+
let up = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)
|
|
456
|
+
else {
|
|
457
|
+
throw HelperError.commandFailed("press action failed", details: ["reason": "event_creation_failed"])
|
|
458
|
+
}
|
|
459
|
+
move.post(tap: .cghidEventTap)
|
|
460
|
+
down.post(tap: .cghidEventTap)
|
|
461
|
+
up.post(tap: .cghidEventTap)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private func captureSurfaceScreenshot(surface: String?, outPath: String, fullscreen: Bool) throws {
|
|
465
|
+
_ = fullscreen
|
|
466
|
+
guard #available(macOS 15.2, *) else {
|
|
467
|
+
throw HelperError.commandFailed(
|
|
468
|
+
"screenshot on macOS desktop and menubar surfaces requires macOS 15.2 or newer"
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
guard let screenFrame = NSScreen.main?.frame, screenFrame.width > 0, screenFrame.height > 0 else {
|
|
472
|
+
throw HelperError.commandFailed("screenshot could not resolve main screen bounds")
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
let rect = CGRect(origin: screenFrame.origin, size: screenFrame.size)
|
|
476
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
477
|
+
var capturedImage: CGImage?
|
|
478
|
+
var capturedError: Error?
|
|
479
|
+
SCScreenshotManager.captureImage(in: rect) { image, error in
|
|
480
|
+
capturedImage = image
|
|
481
|
+
capturedError = error
|
|
482
|
+
semaphore.signal()
|
|
483
|
+
}
|
|
484
|
+
semaphore.wait()
|
|
485
|
+
|
|
486
|
+
if let error = capturedError as NSError? {
|
|
487
|
+
if error.domain == "com.apple.ScreenCaptureKit.SCStreamErrorDomain", error.code == -3801 {
|
|
488
|
+
throw HelperError.commandFailed(
|
|
489
|
+
"screenshot requires Screen Recording permission on macOS desktop and menubar surfaces",
|
|
490
|
+
details: ["surface": surface ?? "", "permission": "screen-recording"]
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
throw HelperError.commandFailed("screenshot failed", details: ["error": error.localizedDescription])
|
|
494
|
+
}
|
|
495
|
+
guard let capturedImage else {
|
|
496
|
+
throw HelperError.commandFailed("screenshot failed")
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let outputURL = URL(fileURLWithPath: outPath)
|
|
500
|
+
if let parent = outputURL.deletingLastPathComponent().path.removingPercentEncoding, !parent.isEmpty {
|
|
501
|
+
try FileManager.default.createDirectory(atPath: parent, withIntermediateDirectories: true)
|
|
502
|
+
}
|
|
503
|
+
guard let destination = CGImageDestinationCreateWithURL(
|
|
504
|
+
outputURL as CFURL,
|
|
505
|
+
UTType.png.identifier as CFString,
|
|
506
|
+
1,
|
|
507
|
+
nil
|
|
508
|
+
) else {
|
|
509
|
+
throw HelperError.commandFailed("screenshot could not create PNG destination")
|
|
510
|
+
}
|
|
511
|
+
CGImageDestinationAddImage(destination, capturedImage, nil)
|
|
512
|
+
guard CGImageDestinationFinalize(destination) else {
|
|
513
|
+
throw HelperError.commandFailed("screenshot could not write PNG file")
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
398
517
|
private func readableText(for element: AXUIElement) -> String? {
|
|
399
518
|
return
|
|
400
519
|
stringAttribute(element, attribute: kAXValueAttribute as String)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-device",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Unified control plane for physical and virtual devices via an agent-driven CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Callstack",
|
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
"build:macos-helper": "swift build -c release --package-path macos-helper",
|
|
31
31
|
"build:all": "pnpm build:node && pnpm build:xcuitest",
|
|
32
32
|
"ad": "node bin/agent-device.mjs",
|
|
33
|
-
"
|
|
33
|
+
"lint": "oxlint . --deny-warnings",
|
|
34
|
+
"format": "oxfmt --write src test skills package.json tsconfig.json .oxlintrc.json .oxfmtrc.json",
|
|
35
|
+
"check:quick": "pnpm lint && pnpm typecheck",
|
|
36
|
+
"check:tooling": "pnpm lint && pnpm typecheck && pnpm build",
|
|
37
|
+
"check:unit": "pnpm test:unit && pnpm test:smoke",
|
|
38
|
+
"check": "pnpm check:tooling && pnpm check:unit",
|
|
34
39
|
"prepack": "pnpm build:all",
|
|
35
40
|
"typecheck": "tsc -p tsconfig.json",
|
|
36
41
|
"test": "vitest run",
|
|
@@ -66,22 +71,17 @@
|
|
|
66
71
|
"ios",
|
|
67
72
|
"android"
|
|
68
73
|
],
|
|
69
|
-
"prettier": {
|
|
70
|
-
"singleQuote": true,
|
|
71
|
-
"semi": true,
|
|
72
|
-
"trailingComma": "all",
|
|
73
|
-
"printWidth": 100
|
|
74
|
-
},
|
|
75
74
|
"dependencies": {
|
|
76
75
|
"pngjs": "^7.0.0"
|
|
77
76
|
},
|
|
78
77
|
"devDependencies": {
|
|
79
78
|
"@microsoft/api-extractor": "^7.52.10",
|
|
80
|
-
"@rslib/core": "0.
|
|
79
|
+
"@rslib/core": "0.20.1",
|
|
81
80
|
"@types/node": "^22.0.0",
|
|
82
81
|
"@types/pngjs": "^6.0.5",
|
|
83
|
-
"
|
|
84
|
-
"
|
|
82
|
+
"oxfmt": "^0.42.0",
|
|
83
|
+
"oxlint": "^1.57.0",
|
|
84
|
+
"typescript": "^6.0.2",
|
|
85
85
|
"vitest": "^4.1.2"
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -30,7 +30,8 @@ agent-device close
|
|
|
30
30
|
- `app`: default surface and the normal choice for `click`, `fill`, `press`, `scroll`, `screenshot`, and `record`.
|
|
31
31
|
- `frontmost-app`: inspect the currently focused app without naming it first.
|
|
32
32
|
- `desktop`: inspect visible desktop windows across apps.
|
|
33
|
-
- `menubar`: inspect the active app menu bar and system menu extras.
|
|
33
|
+
- `menubar`: inspect the active app menu bar and system menu extras. Use `open <app> --platform macos --surface menubar` when you need one menu bar app's extras, such as a status-item app.
|
|
34
|
+
- Menu bar apps can expose a sparse or empty default `app` tree. Prefer the `menubar` surface first when the app lives entirely in the top bar.
|
|
34
35
|
|
|
35
36
|
Use inspect-first surfaces to understand desktop-global UI, then switch back to `app` when you need to act in one app.
|
|
36
37
|
|
|
@@ -81,6 +82,6 @@ Troubleshooting:
|
|
|
81
82
|
|
|
82
83
|
- If visible content is missing from `snapshot -i`, re-snapshot after the UI settles.
|
|
83
84
|
- If `desktop` is too broad, retry with `frontmost-app`.
|
|
84
|
-
- If `menubar` is missing the expected menu, make the app frontmost first and retry.
|
|
85
|
+
- If `menubar` is missing the expected menu, retry with `open <app> --platform macos --surface menubar` for menu bar apps, or make the app frontmost first and retry the generic menubar surface.
|
|
85
86
|
- If the wrong menu opened, retry secondary-clicking the row or cell wrapper rather than the nested text node.
|
|
86
87
|
- If the app has multiple windows, make the correct window frontmost before relying on refs.
|
package/dist/src/306.js
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{AsyncLocalStorage as e}from"node:async_hooks";import t,{createHash as r}from"node:crypto";import n,{existsSync as i,promises as o}from"node:fs";import a from"node:os";import s from"node:path";import{PNG as l}from"pngjs";import{URL as d,fileURLToPath as u,pathToFileURL as c}from"node:url";import{spawn as p,spawnSync as f}from"node:child_process";let m=new e,h=/(token|secret|password|authorization|cookie|api[_-]?key|access[_-]?key|private[_-]?key)/i,g=/(bearer\s+[a-z0-9._-]+|(?:api[_-]?key|token|secret|password)\s*[=:]\s*\S+)/i;function w(){return t.randomBytes(8).toString("hex")}async function I(e,r){let n={...e,diagnosticId:`${Date.now().toString(36)}-${t.randomBytes(4).toString("hex")}`,events:[]};return await m.run(n,r)}function S(){let e=m.getStore();return e?{diagnosticId:e.diagnosticId,requestId:e.requestId,session:e.session,command:e.command,debug:e.debug}:{}}function v(e){let t=m.getStore();if(!t)return;let r={ts:new Date().toISOString(),level:e.level??"info",phase:e.phase,session:t.session,requestId:t.requestId,command:t.command,durationMs:e.durationMs,data:e.data?b(e.data):void 0};if(t.events.push(r),!t.debug)return;let i=`[agent-device][diag] ${JSON.stringify(r)}
|
|
2
|
-
`;try{t.logPath&&n.appendFile(t.logPath,i,()=>{}),t.traceLogPath&&n.appendFile(t.traceLogPath,i,()=>{}),t.logPath||t.traceLogPath||process.stderr.write(i)}catch{}}async function y(e,t,r){let n=Date.now();try{let i=await t();return v({level:"info",phase:e,durationMs:Date.now()-n,data:r}),i}catch(t){throw v({level:"error",phase:e,durationMs:Date.now()-n,data:{...r??{},error:t instanceof Error?t.message:String(t)}}),t}}function A(e={}){let t=m.getStore();if(!t||!e.force&&!t.debug||0===t.events.length)return null;try{let e=(t.session??"default").replace(/[^a-zA-Z0-9._-]/g,"_"),r=new Date().toISOString().slice(0,10),i=s.join(a.homedir(),".agent-device","logs",e,r);n.mkdirSync(i,{recursive:!0});let o=new Date().toISOString().replace(/[:.]/g,"-"),l=s.join(i,`${o}-${t.diagnosticId}.ndjson`),d=t.events.map(e=>JSON.stringify(b(e)));return n.writeFileSync(l,`${d.join("\n")}
|
|
3
|
-
`),t.events=[],l}catch{return null}}function b(e){return function e(t,r,n){if(null==t)return t;if("string"==typeof t){var i=t,o=n;let e=i.trim();if(!e)return i;if(o&&h.test(o)||g.test(e))return"[REDACTED]";let r=function(e){try{let t=new URL(e);return t.search&&(t.search="?REDACTED"),(t.username||t.password)&&(t.username="REDACTED",t.password="REDACTED"),t.toString()}catch{return null}}(e);return r||(e.length>400?`${e.slice(0,200)}...<truncated>`:e)}if("object"!=typeof t)return t;if(r.has(t))return"[Circular]";if(r.add(t),Array.isArray(t))return t.map(t=>e(t,r));let a={};for(let[n,i]of Object.entries(t)){if(h.test(n)){a[n]="[REDACTED]";continue}a[n]=e(i,r,n)}return a}(e,new WeakSet)}class E extends Error{code;details;cause;constructor(e,t,r,n){super(t),this.code=e,this.details=r,this.cause=n}}function $(e){return e instanceof E?e:e instanceof Error?new E("UNKNOWN",e.message,void 0,e):new E("UNKNOWN","Unknown error",{err:e})}function N(e,t={}){let r=$(e),n=r.details?b(r.details):void 0,i=n&&"string"==typeof n.hint?n.hint:void 0,o=(n&&"string"==typeof n.diagnosticId?n.diagnosticId:void 0)??t.diagnosticId,a=(n&&"string"==typeof n.logPath?n.logPath:void 0)??t.logPath,s=i??function(e){switch(e){case"INVALID_ARGS":return"Check command arguments and run --help for usage examples.";case"SESSION_NOT_FOUND":return"Run open first or pass an explicit device selector.";case"TOOL_MISSING":return"Install required platform tooling and ensure it is available in PATH.";case"DEVICE_NOT_FOUND":return"Verify the target device is booted/connected and selectors match.";case"UNSUPPORTED_OPERATION":return"This command is not available for the selected platform/device.";case"COMMAND_FAILED":default:return"Retry with --debug and inspect diagnostics log for details.";case"UNAUTHORIZED":return"Refresh daemon metadata and retry the command."}}(r.code),l=function(e){if(!e)return;let t={...e};return delete t.hint,delete t.diagnosticId,delete t.logPath,Object.keys(t).length>0?t:void 0}(n),d=function(e,t,r){if("COMMAND_FAILED"!==e||r?.processExitError!==!0)return t;let n=function(e){let t=[/^an error was encountered processing the command/i,/^underlying error\b/i,/^simulator device failed to complete the requested operation/i];for(let r of e.split("\n")){let e=r.trim();if(e&&!t.some(t=>t.test(e)))return e.length>200?`${e.slice(0,200)}...`:e}return null}("string"==typeof r?.stderr?r.stderr:"");return n||t}(r.code,r.message,n);return{code:r.code,message:d,hint:s,diagnosticId:o,logPath:a,details:l}}let D="<wifi|airplane|location> <on|off>",x="appearance <light|dark|toggle>",_="faceid <match|nonmatch|enroll|unenroll>",T="touchid <match|nonmatch|enroll|unenroll>",L="fingerprint <match|nonmatch>",O="permission <grant|deny|reset> <camera|microphone|photos|contacts|contacts-limited|notifications|calendar|location|location-always|media-library|motion|reminders|siri> [full|limited]",M="permission <grant|reset> <accessibility|screen-recording|input-monitoring>",C=`settings ${D} | settings ${x} | settings ${_} | settings ${T} | settings ${L} | settings ${O} | settings ${M}`,k=`settings requires ${D}, ${x}, ${_}, ${T}, ${L}, ${O}, or ${M}`,P=["app","frontmost-app","desktop","menubar"];function R(e){let t=e?.trim().toLowerCase();if("app"===t||"frontmost-app"===t||"desktop"===t||"menubar"===t)return t;throw new E("INVALID_ARGS",`Invalid surface: ${e}. Use ${P.join("|")}.`)}function F(e){var t;let r,n=B(e.label),i=B(e.value),o=B(e.identifier),a=(t=o)&&!/^[\w.]+:id\/[\w.-]+$/i.test(t)&&!/^_?NS:\d+$/i.test(t)?o:"";return(r=j(e.type??"")).includes("textfield")||r.includes("securetextfield")||r.includes("searchfield")||r.includes("edittext")||r.includes("textview")||r.includes("textarea")?i||n||a:n||i||a}function B(e){return"string"==typeof e?e.trim():""}function j(e){let t=e.trim().replace(/XCUIElementType/gi,"").replace(/^AX/,"").toLowerCase(),r=Math.max(t.lastIndexOf("."),t.lastIndexOf("/"));return -1!==r&&(t=t.slice(r+1)),t}function G(e,t={}){let r=[],n=[];for(let i of e){let e=i.depth??0;for(;r.length>0&&e<=r[r.length-1];)r.pop();let o=i.label?.trim()||i.value?.trim()||i.identifier?.trim()||"",a=V(i.type??"Element"),s="group"===a&&!o;s&&r.push(e);let l=s?e:Math.max(0,e-r.length);n.push({node:i,depth:l,type:a,text:z(i,l,s,a,t)})}return n}function z(e,t,r,n,i={}){var o,a,s,l,d;let u,c,p=n??V(e.type??"Element"),f=(u=F(e),{text:u,isLargeSurface:c=function(e,t){if("text-view"===t||"text-field"===t||"search"===t)return!0;let r=j(e.type??""),n=`${e.role??""} ${e.subrole??""}`.toLowerCase();return r.includes("textview")||r.includes("textarea")||r.includes("textfield")||r.includes("securetextfield")||r.includes("searchfield")||r.includes("edittext")||n.includes("text area")||n.includes("text field")}(e,p),shouldSummarize:c&&!!(o=u)&&(o.length>80||/[\r\n]/.test(o))}),m=(a=e,s=p,l=i,d=f,l.summarizeTextSurfaces&&d.shouldSummarize&&function(e,t,r){let n=B(e.label);if(n&&n!==r)return n;let i=B(e.identifier);if(i&&!q(i)&&i!==r)return i;switch(t){case"text":case"text-view":return"Text view";case"text-field":return"Text field";case"search":return"Search field";default:return""}}(a,s,d.text)||U(a,s)),h=" ".repeat(t),g=e.ref?`@${e.ref}`:"",w=(function(e,t,r,n){let i,o=[];if(!1===e.enabled&&o.push("disabled"),!r.summarizeTextSurfaces||(!0===e.selected&&o.push("selected"),H(t)&&o.push("editable"),function(e,t){if("scroll-area"===t)return!0;let r=(e.type??"").toLowerCase(),n=`${e.role??""} ${e.subrole??""}`.toLowerCase();return r.includes("scroll")||n.includes("scroll")}(e,t)&&o.push("scrollable"),!n.shouldSummarize))return o;return o.push(`preview:"${((i=n.text.replace(/\s+/g," ").trim()).length<=48?i:`${i.slice(0,45)}...`).replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`),o.push("truncated"),[...new Set(o)]})(e,p,i,f).map(e=>` [${e}]`).join(""),I=m?` "${m}"`:"";return r?`${h}${g} [${p}]${w}`.trimEnd():`${h}${g} [${p}]${I}${w}`.trimEnd()}function U(e,t){let r=e.label?.trim(),n=e.value?.trim();if(H(t)){if(n)return n;if(r)return r}else if(r)return r;if(n)return n;let i=e.identifier?.trim();return!i||q(i)&&("group"===t||"image"===t||"list"===t||"collection"===t)?"":i}function V(e){let t=e.replace(/XCUIElementType/gi,"").toLowerCase(),r=e.includes(".")&&(e.startsWith("android.")||e.startsWith("androidx.")||e.startsWith("com."));switch(t.includes(".")&&(t=t.replace(/^android\.widget\./,"").replace(/^android\.view\./,"").replace(/^android\.webkit\./,"").replace(/^androidx\./,"").replace(/^com\.google\.android\./,"").replace(/^com\.android\./,"")),t){case"application":return"application";case"navigationbar":return"navigation-bar";case"tabbar":return"tab-bar";case"button":case"imagebutton":return"button";case"link":return"link";case"cell":return"cell";case"statictext":case"checkedtextview":return"text";case"textfield":case"edittext":return"text-field";case"textview":return r?"text":"text-view";case"textarea":return"text-view";case"switch":return"switch";case"slider":return"slider";case"image":case"imageview":return"image";case"webview":return"webview";case"framelayout":case"linearlayout":case"relativelayout":case"constraintlayout":case"viewgroup":case"view":case"group":return"group";case"listview":case"recyclerview":return"list";case"collectionview":return"collection";case"searchfield":return"search";case"segmentedcontrol":return"segmented-control";case"window":return"window";case"checkbox":return"checkbox";case"radio":return"radio";case"menuitem":return"menu-item";case"toolbar":return"toolbar";case"scrollarea":case"scrollview":case"nestedscrollview":return"scroll-area";case"table":return"table";default:return t||"element"}}function H(e){return"text-field"===e||"text-view"===e||"search"===e}function q(e){return/^[\w.]+:id\/[\w.-]+$/i.test(e)}function J(){try{let e=W();return JSON.parse(n.readFileSync(s.join(e,"package.json"),"utf8")).version??"0.0.0"}catch{return"0.0.0"}}function W(){let e=s.dirname(u(import.meta.url)),t=e;for(let e=0;e<6;e+=1){let e=s.join(t,"package.json");if(n.existsSync(e))return t;t=s.dirname(t)}return e}function K(e){return e?{message:e}:{}}function X(e,t){return t?{...e,message:t}:e}function Z(e){return"string"==typeof e?.message&&e.message.length>0?e.message:null}async function Q(e,t,r={}){return new Promise((n,i)=>{let o=p(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached}),a="",s=r.binaryStdout?Buffer.alloc(0):void 0,l="",d=!1,u=ei(r.timeoutMs),c=u?setTimeout(()=>{d=!0,o.kill("SIGKILL")},u):null;r.binaryStdout||o.stdout.setEncoding("utf8"),o.stderr.setEncoding("utf8"),void 0!==r.stdin&&o.stdin.write(r.stdin),o.stdin.end(),o.stdout.on("data",e=>{r.binaryStdout?s=Buffer.concat([s??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]):a+=e}),o.stderr.on("data",e=>{l+=e}),o.on("error",r=>{(c&&clearTimeout(c),"ENOENT"===r.code)?i(new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):i(new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),o.on("close",o=>{c&&clearTimeout(c);let p=o??1;d&&u?i(new E("COMMAND_FAILED",`${e} timed out after ${u}ms`,{cmd:e,args:t,stdout:a,stderr:l,exitCode:p,timeoutMs:u})):0===p||r.allowFailure?n({stdout:a,stderr:l,exitCode:p,stdoutBuffer:s}):i(new E("COMMAND_FAILED",`${e} exited with code ${p}`,{cmd:e,args:t,stdout:a,stderr:l,exitCode:p,processExitError:!0}))})})}async function Y(e){try{var t;let{shell:r,args:n}=(t=e,"win32"===process.platform?{shell:"cmd.exe",args:["/c","where",t]}:{shell:"bash",args:["-lc",`command -v ${t}`]}),i=await Q(r,n,{allowFailure:!0});return 0===i.exitCode&&i.stdout.trim().length>0}catch{return!1}}function ee(e,t,r={}){let n=f(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],encoding:r.binaryStdout?void 0:"utf8",input:r.stdin,timeout:ei(r.timeoutMs)});if(n.error){let i=n.error.code;if("ETIMEDOUT"===i)throw new E("COMMAND_FAILED",`${e} timed out after ${ei(r.timeoutMs)}ms`,{cmd:e,args:t,timeoutMs:ei(r.timeoutMs)},n.error);if("ENOENT"===i)throw new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},n.error);throw new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},n.error)}let i=r.binaryStdout?Buffer.isBuffer(n.stdout)?n.stdout:Buffer.from(n.stdout??""):void 0,o=r.binaryStdout?"":"string"==typeof n.stdout?n.stdout:(n.stdout??"").toString(),a="string"==typeof n.stderr?n.stderr:(n.stderr??"").toString(),s=n.status??1;if(0!==s&&!r.allowFailure)throw new E("COMMAND_FAILED",`${e} exited with code ${s}`,{cmd:e,args:t,stdout:o,stderr:a,exitCode:s,processExitError:!0});return{stdout:o,stderr:a,exitCode:s,stdoutBuffer:i}}function et(e,t,r={}){p(e,t,{cwd:r.cwd,env:r.env,stdio:"ignore",detached:!0}).unref()}async function er(e,t,r={}){return new Promise((n,i)=>{let o=p(e,t,{cwd:r.cwd,env:r.env,stdio:["pipe","pipe","pipe"],detached:r.detached});r.onSpawn?.(o);let a="",s="",l=r.binaryStdout?Buffer.alloc(0):void 0;r.binaryStdout||o.stdout.setEncoding("utf8"),o.stderr.setEncoding("utf8"),void 0!==r.stdin&&o.stdin.write(r.stdin),o.stdin.end(),o.stdout.on("data",e=>{if(r.binaryStdout){l=Buffer.concat([l??Buffer.alloc(0),Buffer.isBuffer(e)?e:Buffer.from(e)]);return}let t=String(e);a+=t,r.onStdoutChunk?.(t)}),o.stderr.on("data",e=>{let t=String(e);s+=t,r.onStderrChunk?.(t)}),o.on("error",r=>{"ENOENT"===r.code?i(new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):i(new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),o.on("close",o=>{let d=o??1;0===d||r.allowFailure?n({stdout:a,stderr:s,exitCode:d,stdoutBuffer:l}):i(new E("COMMAND_FAILED",`${e} exited with code ${d}`,{cmd:e,args:t,stdout:a,stderr:s,exitCode:d,processExitError:!0}))})})}function en(e,t,r={}){let n=p(e,t,{cwd:r.cwd,env:r.env,stdio:["ignore","pipe","pipe"],detached:r.detached}),i="",o="";n.stdout.setEncoding("utf8"),n.stderr.setEncoding("utf8"),n.stdout.on("data",e=>{i+=e}),n.stderr.on("data",e=>{o+=e});let a=new Promise((a,s)=>{n.on("error",r=>{"ENOENT"===r.code?s(new E("TOOL_MISSING",`${e} not found in PATH`,{cmd:e},r)):s(new E("COMMAND_FAILED",`Failed to run ${e}`,{cmd:e,args:t},r))}),n.on("close",n=>{let l=n??1;0===l||r.allowFailure?a({stdout:i,stderr:o,exitCode:l}):s(new E("COMMAND_FAILED",`${e} exited with code ${l}`,{cmd:e,args:t,stdout:i,stderr:o,exitCode:l,processExitError:!0}))})});return{child:n,wait:a}}function ei(e){if(!Number.isFinite(e))return;let t=Math.floor(e);if(!(t<=0))return t}let eo=[/(^|[\/\s"'=])dist\/src\/daemon\.js($|[\s"'])/,/(^|[\/\s"'=])src\/daemon\.ts($|[\s"'])/];function ea(e){if(!Number.isInteger(e)||e<=0)return!1;try{return process.kill(e,0),!0}catch(e){return"EPERM"===e.code}}function es(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=ee("ps",["-p",String(e),"-o","lstart="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function el(e){if(!Number.isInteger(e)||e<=0)return null;try{let t=ee("ps",["-p",String(e),"-o","command="],{allowFailure:!0,timeoutMs:1e3});if(0!==t.exitCode)return null;let r=t.stdout.trim();return r.length>0?r:null}catch{return null}}function ed(e,t){let r;if(!ea(e))return!1;if(t){let r=es(e);if(!r||r!==t)return!1}let n=el(e);return!!n&&!!(r=n.toLowerCase().replaceAll("\\","/")).includes("agent-device")&&eo.some(e=>e.test(r))}function eu(e,t){try{return process.kill(e,t),!0}catch(t){let e=t.code;if("ESRCH"===e||"EPERM"===e)return!1;throw t}}async function ec(e,t){if(!ea(e))return!0;let r=Date.now();for(;Date.now()-r<t;)if(await new Promise(e=>setTimeout(e,50)),!ea(e))return!0;return!ea(e)}async function ep(e,t){!ed(e,t.expectedStartTime)||!eu(e,"SIGTERM")||await ec(e,t.termTimeoutMs)||eu(e,"SIGKILL")&&await ec(e,t.killTimeoutMs)}function ef(e){return e?.HOME?.trim()||a.homedir()}function em(e,t={}){return"~"===e?ef(t.env):e.startsWith("~/")?s.join(ef(t.env),e.slice(2)):e}function eh(e,t={}){let r=em(e,t);return s.isAbsolute(r)?r:s.resolve(t.cwd??process.cwd(),r)}function eg(e){let t,r=(t=(e??"").trim())?eh(t):s.join(em("~"),".agent-device");return{baseDir:r,infoPath:s.join(r,"daemon.json"),lockPath:s.join(r,"daemon.lock"),logPath:s.join(r,"daemon.log"),sessionsDir:s.join(r,"sessions")}}function ew(e){let t=(e??"").trim().toLowerCase();return"http"===t?"http":"dual"===t?"dual":"socket"}function eI(e){let t=(e??"").trim().toLowerCase();return"auto"===t?"auto":"socket"===t?"socket":"http"===t?"http":"auto"}function eS(e){return"tenant"===(e??"").trim().toLowerCase()?"tenant":"none"}function ev(e){if(!e)return;let t=e.trim();if(t&&/^[a-zA-Z0-9._-]{1,128}$/.test(t))return t}let ey=100,eA=new Set(["batch","replay"]),eb=new Set(["command","positionals","flags","runtime"]);function eE(e){let t;try{t=JSON.parse(e)}catch{throw new E("INVALID_ARGS","Batch steps must be valid JSON.")}if(!Array.isArray(t)||0===t.length)throw new E("INVALID_ARGS","Batch steps must be a non-empty JSON array.");return t}function e$(e,t){if(!Array.isArray(e)||0===e.length)throw new E("INVALID_ARGS","batch requires a non-empty batchSteps array.");if(e.length>t)throw new E("INVALID_ARGS",`batch has ${e.length} steps; max allowed is ${t}.`);let r=[];for(let t=0;t<e.length;t+=1){let n=e[t];if(!n||"object"!=typeof n)throw new E("INVALID_ARGS",`Invalid batch step at index ${t}.`);let i=Object.keys(n).filter(e=>!eb.has(e));if(i.length>0){let e=i.map(e=>`"${e}"`).join(", ");throw new E("INVALID_ARGS",`Batch step ${t+1} has unknown field(s): ${e}. Allowed fields: command, positionals, flags, runtime.`)}let o="string"==typeof n.command?n.command.trim().toLowerCase():"";if(!o)throw new E("INVALID_ARGS",`Batch step ${t+1} requires command.`);if(eA.has(o))throw new E("INVALID_ARGS",`Batch step ${t+1} cannot run ${o}.`);if(void 0!==n.positionals&&!Array.isArray(n.positionals))throw new E("INVALID_ARGS",`Batch step ${t+1} positionals must be an array.`);let a=n.positionals??[];if(a.some(e=>"string"!=typeof e))throw new E("INVALID_ARGS",`Batch step ${t+1} positionals must contain only strings.`);if(void 0!==n.flags&&("object"!=typeof n.flags||Array.isArray(n.flags)||!n.flags))throw new E("INVALID_ARGS",`Batch step ${t+1} flags must be an object.`);if(void 0!==n.runtime&&("object"!=typeof n.runtime||Array.isArray(n.runtime)||!n.runtime))throw new E("INVALID_ARGS",`Batch step ${t+1} runtime must be an object.`);r.push({command:o,positionals:a,flags:n.flags??{},runtime:n.runtime})}return r}function eN(e){let t=e.appId??e.bundleId??e.packageName;return{session:e.session,appId:t,appBundleId:e.bundleId,package:e.packageName}}function eD(e,t,r){return{deviceId:t,deviceName:r,..."android"===e?{serial:t}:"ios"===e?{udid:t}:{}}}function ex(e,t={}){let r=t.includeAndroidSerial??!0;return{platform:e.platform,target:e.target,device:e.name,id:e.id,..."ios"===e.platform?{device_udid:e.ios?.udid??e.id,ios_simulator_device_set:e.ios?.simulatorSetPath??null}:{},..."android"===e.platform&&r?{serial:e.android?.serial??e.id}:{}}}function e_(e){return{name:e.name,...ex(e.device,{includeAndroidSerial:!1}),createdAt:e.createdAt}}function eT(e){return{platform:e.platform,id:e.id,name:e.name,kind:e.kind,target:e.target,..."boolean"==typeof e.booted?{booted:e.booted}:{}}}function eL(e){let t=e.created?"Created":"Reused",r=e.booted?" (booted)":"";return X({udid:e.udid,device:e.device,runtime:e.runtime,ios_simulator_device_set:e.iosSimulatorDeviceSet??null,created:e.created,booted:e.booted},`${t}: ${e.device} ${e.udid}${r}`)}function eO(e){return e.bundleId??e.package??e.app}function eM(e){return X({app:e.app,appPath:e.appPath,platform:e.platform,...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.package?{package:e.package}:{}},`Installed: ${eO(e)}`)}function eC(e){return e.appName??e.bundleId??e.packageName??e.launchTarget}function ek(e){return X({launchTarget:e.launchTarget,...e.appName?{appName:e.appName}:{},...e.appId?{appId:e.appId}:{},...e.bundleId?{bundleId:e.bundleId}:{},...e.packageName?{package:e.packageName}:{},...e.installablePath?{installablePath:e.installablePath}:{},...e.archivePath?{archivePath:e.archivePath}:{},...e.materializationId?{materializationId:e.materializationId}:{},...e.materializationExpiresAt?{materializationExpiresAt:e.materializationExpiresAt}:{}},`Installed: ${eC(e)}`)}function eP(e){let t=e.appName??e.appBundleId??e.session;return X({session:e.session,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{},...e.startup?{startup:e.startup}:{},...e.runtime?{runtime:e.runtime}:{},...e.device?ex(e.device):{}},t?`Opened: ${t}`:"Opened")}function eR(e){return{session:e.session,...e.shutdown?{shutdown:e.shutdown}:{},...K(e.session?`Closed: ${e.session}`:"Closed")}}function eF(e){return{nodes:e.nodes,truncated:e.truncated,...e.appName?{appName:e.appName}:{},...e.appBundleId?{appBundleId:e.appBundleId}:{}}}function eB(e,t){try{return l.sync.read(e)}catch(e){throw new E("COMMAND_FAILED",`Failed to decode ${t} as PNG`,{label:t,reason:e instanceof Error?e.message:String(e)})}}export{TextDecoder,styleText}from"node:util";export{default as node_net}from"node:net";export{default as node_http}from"node:http";export{default as node_https}from"node:https";export{E as AppError,ey as DEFAULT_BATCH_MAX_STEPS,l as PNG,P as SESSION_SURFACES,k as SETTINGS_INVALID_ARGS_MESSAGE,C as SETTINGS_USAGE_OVERRIDE,$ as asAppError,eN as buildAppIdentifiers,eD as buildDeviceIdentifiers,G as buildSnapshotDisplayLines,r as createHash,w as createRequestId,eB as decodePng,U as displayLabel,v as emitDiagnostic,i as existsSync,em as expandUserHomePath,d as external_node_url_URL,F as extractReadableText,u as fileURLToPath,W as findProjectRoot,A as flushDiagnosticsToSessionFile,V as formatRole,z as formatSnapshotLine,S as getDiagnosticsMeta,ed as isAgentDeviceDaemonProcess,ea as isProcessAlive,t as node_crypto,n as node_fs,a as node_os,s as node_path,N as normalizeError,ev as normalizeTenantId,eE as parseBatchStepsJson,R as parseSessionSurface,c as pathToFileURL,o as promises,Z as readCommandMessage,el as readProcessCommand,es as readProcessStartTime,J as readVersion,eg as resolveDaemonPaths,ew as resolveDaemonServerMode,eI as resolveDaemonTransportPreference,eO as resolveDeployResultTarget,eC as resolveInstallFromSourceResultTarget,eS as resolveSessionIsolationMode,eh as resolveUserPath,Q as runCmd,en as runCmdBackground,et as runCmdDetached,er as runCmdStreaming,ee as runCmdSync,eR as serializeCloseResult,eM as serializeDeployResult,eT as serializeDevice,eL as serializeEnsureSimulatorResult,ek as serializeInstallFromSourceResult,eP as serializeOpenResult,e_ as serializeSessionListEntry,eF as serializeSnapshotResult,p as spawn,ep as stopProcessForTakeover,K as successText,e$ as validateAndNormalizeBatchSteps,Y as whichCmd,y as withDiagnosticTimer,I as withDiagnosticsScope,X as withSuccessText};
|