mcp-baepsae 6.2.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-KR.md +4 -4
- package/README.md +4 -4
- package/bundled/baepsae-native +0 -0
- package/dist/tool-manifest.d.ts.map +1 -1
- package/dist/tool-manifest.js +12 -0
- package/dist/tool-manifest.js.map +1 -1
- package/dist/tools/input.d.ts.map +1 -1
- package/dist/tools/input.js +18 -4
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/system.d.ts.map +1 -1
- package/dist/tools/system.js +52 -0
- package/dist/tools/system.js.map +1 -1
- package/dist/tools/ui.d.ts.map +1 -1
- package/dist/tools/ui.js +125 -6
- package/dist/tools/ui.js.map +1 -1
- package/dist/utils.d.ts +9 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +57 -2
- package/dist/utils.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/native/Sources/Commands/InputCommands.swift +1 -1
- package/native/Sources/Commands/InputSourceCommands.swift +80 -0
- package/native/Sources/Commands/SystemCommands.swift +371 -23
- package/native/Sources/Commands/UICommands.swift +428 -7
- package/native/Sources/Utils.swift +78 -17
- package/native/Sources/Version.swift +1 -1
- package/native/Sources/main.swift +44 -0
- package/package.json +1 -1
|
@@ -24,7 +24,16 @@ func handleDescribeUI(_ parsed: ParsedOptions) throws -> Int32 {
|
|
|
24
24
|
// For macOS apps, start from window level, no iOSContentGroup filtering
|
|
25
25
|
if !parsed.flags.contains("--all") {
|
|
26
26
|
let windows = Children(appRoot)
|
|
27
|
-
if let
|
|
27
|
+
if let windowSelector = parsed.options["--window"] {
|
|
28
|
+
if let idx = Int(windowSelector), idx < windows.count {
|
|
29
|
+
targetRoot = windows[idx]
|
|
30
|
+
} else {
|
|
31
|
+
targetRoot = windows.first(where: {
|
|
32
|
+
let title = StringAttribute($0, kAXTitleAttribute as CFString) ?? ""
|
|
33
|
+
return title.localizedCaseInsensitiveContains(windowSelector)
|
|
34
|
+
}) ?? windows.first ?? appRoot
|
|
35
|
+
}
|
|
36
|
+
} else if let firstWindow = windows.first {
|
|
28
37
|
targetRoot = firstWindow
|
|
29
38
|
}
|
|
30
39
|
}
|
|
@@ -72,6 +81,31 @@ func handleDescribeUI(_ parsed: ParsedOptions) throws -> Int32 {
|
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
// Page-size based pagination for large trees
|
|
85
|
+
if let pageSizeStr = parsed.options["--page-size"], let pageSize = Int(pageSizeStr) {
|
|
86
|
+
let page = Int(parsed.options["--page"] ?? "0") ?? 0
|
|
87
|
+
var totalCount: CFIndex = 0
|
|
88
|
+
AXUIElementGetAttributeValueCount(targetRoot, kAXChildrenAttribute as CFString, &totalCount)
|
|
89
|
+
let offset = page * pageSize
|
|
90
|
+
if offset >= Int(totalCount) {
|
|
91
|
+
print("Page \(page) is beyond total children count (\(totalCount)).")
|
|
92
|
+
print("total=\(totalCount) pageSize=\(pageSize) pages=\((Int(totalCount) + pageSize - 1) / max(pageSize, 1))")
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
var pageChildren: CFArray?
|
|
96
|
+
let fetchCount = min(pageSize, Int(totalCount) - offset)
|
|
97
|
+
AXUIElementCopyAttributeValues(targetRoot, kAXChildrenAttribute as CFString, CFIndex(offset), CFIndex(fetchCount), &pageChildren)
|
|
98
|
+
let totalPages = (Int(totalCount) + pageSize - 1) / max(pageSize, 1)
|
|
99
|
+
print("page=\(page)/\(max(totalPages - 1, 0)) total=\(totalCount) pageSize=\(pageSize) showing=\(offset)..\(offset + fetchCount - 1)")
|
|
100
|
+
if let children = pageChildren as? [AXUIElement] {
|
|
101
|
+
for child in children {
|
|
102
|
+
let childLines = describeAccessibilityTree(from: child, options: descOpts)
|
|
103
|
+
print(childLines.joined(separator: "\n"))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
75
109
|
var lines = describeAccessibilityTree(from: targetRoot, options: descOpts)
|
|
76
110
|
if lines.isEmpty {
|
|
77
111
|
throw NativeError.commandFailed("No accessibility elements found.")
|
|
@@ -366,6 +400,34 @@ func handleType(_ parsed: ParsedOptions) throws -> Int32 {
|
|
|
366
400
|
}
|
|
367
401
|
|
|
368
402
|
let methodStr = parsed.options["--method"] ?? "auto"
|
|
403
|
+
|
|
404
|
+
if methodStr == "ax" {
|
|
405
|
+
let appRoot = try accessibilityRootElement(for: target)
|
|
406
|
+
var focusedRef: CFTypeRef?
|
|
407
|
+
let status = AXUIElementCopyAttributeValue(
|
|
408
|
+
appRoot,
|
|
409
|
+
"AXFocusedUIElement" as CFString,
|
|
410
|
+
&focusedRef
|
|
411
|
+
)
|
|
412
|
+
if status == .success, let focused = focusedRef {
|
|
413
|
+
let setResult = AXUIElementSetAttributeValue(
|
|
414
|
+
focused as! AXUIElement,
|
|
415
|
+
kAXValueAttribute as CFString,
|
|
416
|
+
text as CFTypeRef
|
|
417
|
+
)
|
|
418
|
+
if setResult == .success {
|
|
419
|
+
print("Set value via AX API.")
|
|
420
|
+
return 0
|
|
421
|
+
}
|
|
422
|
+
fputs("AX setValue failed (status \(setResult.rawValue)), falling back to paste\n", stderr)
|
|
423
|
+
} else {
|
|
424
|
+
fputs("No focused element found, falling back to paste\n", stderr)
|
|
425
|
+
}
|
|
426
|
+
// Fallback to paste
|
|
427
|
+
try pasteText(text, target: target)
|
|
428
|
+
return 0
|
|
429
|
+
}
|
|
430
|
+
|
|
369
431
|
let usePaste: Bool
|
|
370
432
|
switch methodStr {
|
|
371
433
|
case "paste":
|
|
@@ -373,11 +435,7 @@ func handleType(_ parsed: ParsedOptions) throws -> Int32 {
|
|
|
373
435
|
case "keyboard":
|
|
374
436
|
usePaste = false
|
|
375
437
|
default: // auto
|
|
376
|
-
|
|
377
|
-
usePaste = true
|
|
378
|
-
} else {
|
|
379
|
-
usePaste = false
|
|
380
|
-
}
|
|
438
|
+
usePaste = true // paste is more reliable for all targets including CJK
|
|
381
439
|
}
|
|
382
440
|
|
|
383
441
|
if usePaste {
|
|
@@ -410,7 +468,7 @@ func pasteText(_ text: String, target: TargetApp) throws {
|
|
|
410
468
|
|
|
411
469
|
// Cmd+V: Command keycode=55, V keycode=9
|
|
412
470
|
sendKeyCombo(modifiers: [55], key: 9)
|
|
413
|
-
Thread.sleep(forTimeInterval: 0.
|
|
471
|
+
Thread.sleep(forTimeInterval: 0.3)
|
|
414
472
|
|
|
415
473
|
// Restore original clipboard content
|
|
416
474
|
pasteboard.clearContents()
|
|
@@ -462,6 +520,67 @@ func handleSwipe(_ parsed: ParsedOptions) throws -> Int32 {
|
|
|
462
520
|
return 0
|
|
463
521
|
}
|
|
464
522
|
|
|
523
|
+
// MARK: - detect-dialog
|
|
524
|
+
|
|
525
|
+
func handleDetectDialog(_ parsed: ParsedOptions) throws -> Int32 {
|
|
526
|
+
let target = try resolveTarget(from: parsed)
|
|
527
|
+
try ensureAccessibilityTrusted()
|
|
528
|
+
try activateTarget(target)
|
|
529
|
+
let appRoot = try accessibilityRootElement(for: target)
|
|
530
|
+
let windows = Children(appRoot)
|
|
531
|
+
|
|
532
|
+
var dialogs: [[String: Any]] = []
|
|
533
|
+
for window in windows {
|
|
534
|
+
let subrole = StringAttribute(window, kAXSubroleAttribute as CFString) ?? ""
|
|
535
|
+
let isDialog = ["AXDialog", "AXSheet", "AXSystemDialog", "AXSystemFloatingWindow"].contains(subrole)
|
|
536
|
+
|
|
537
|
+
var modalRef: CFTypeRef?
|
|
538
|
+
let modalStatus = AXUIElementCopyAttributeValue(window, kAXModalAttribute as CFString, &modalRef)
|
|
539
|
+
let isModal = (modalStatus == .success && (modalRef as? Bool) == true)
|
|
540
|
+
|
|
541
|
+
if isDialog || isModal {
|
|
542
|
+
let title = StringAttribute(window, kAXTitleAttribute as CFString) ?? ""
|
|
543
|
+
var buttons: [String] = []
|
|
544
|
+
|
|
545
|
+
func findButtons(in elements: [UIElement]) {
|
|
546
|
+
for element in elements {
|
|
547
|
+
let role = StringAttribute(element, kAXRoleAttribute as CFString)
|
|
548
|
+
if role == "AXButton" {
|
|
549
|
+
if let btnTitle = StringAttribute(element, kAXTitleAttribute as CFString), !btnTitle.isEmpty {
|
|
550
|
+
buttons.append(btnTitle)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
findButtons(in: Children(element))
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
findButtons(in: Children(window))
|
|
557
|
+
|
|
558
|
+
dialogs.append([
|
|
559
|
+
"type": subrole.isEmpty ? "modal" : subrole,
|
|
560
|
+
"title": title,
|
|
561
|
+
"isModal": isModal,
|
|
562
|
+
"buttons": buttons,
|
|
563
|
+
])
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if dialogs.isEmpty {
|
|
568
|
+
print("{\"hasDialog\":false,\"dialogs\":[]}")
|
|
569
|
+
} else {
|
|
570
|
+
var jsonParts: [String] = []
|
|
571
|
+
for d in dialogs {
|
|
572
|
+
let type = d["type"] as? String ?? ""
|
|
573
|
+
let title = (d["title"] as? String ?? "").replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
|
|
574
|
+
let isModal = d["isModal"] as? Bool ?? false
|
|
575
|
+
let btns = d["buttons"] as? [String] ?? []
|
|
576
|
+
let btnsJson = "[" + btns.map { "\"\($0.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\"" }.joined(separator: ",") + "]"
|
|
577
|
+
jsonParts.append("{\"type\":\"\(type)\",\"title\":\"\(title)\",\"isModal\":\(isModal),\"buttons\":\(btnsJson)}")
|
|
578
|
+
}
|
|
579
|
+
print("{\"hasDialog\":true,\"dialogs\":[\(jsonParts.joined(separator: ","))]}")
|
|
580
|
+
}
|
|
581
|
+
return 0
|
|
582
|
+
}
|
|
583
|
+
|
|
465
584
|
func handleScroll(_ parsed: ParsedOptions) throws -> Int32 {
|
|
466
585
|
let target = try resolveTarget(from: parsed)
|
|
467
586
|
try ensureAccessibilityTrusted()
|
|
@@ -503,3 +622,305 @@ func handleScroll(_ parsed: ParsedOptions) throws -> Int32 {
|
|
|
503
622
|
}
|
|
504
623
|
return 0
|
|
505
624
|
}
|
|
625
|
+
|
|
626
|
+
// MARK: - handleSetUIValue (F010)
|
|
627
|
+
|
|
628
|
+
func handleSetUIValue(_ parsed: ParsedOptions) throws -> Int32 {
|
|
629
|
+
let target = try resolveTarget(from: parsed)
|
|
630
|
+
try ensureAccessibilityTrusted()
|
|
631
|
+
try activateTarget(target)
|
|
632
|
+
let appRoot = try accessibilityRootElement(for: target)
|
|
633
|
+
|
|
634
|
+
let attributeStr = parsed.options["--attribute"] ?? "value"
|
|
635
|
+
let valueStr = try requiredOption("--value", from: parsed)
|
|
636
|
+
|
|
637
|
+
// Find element
|
|
638
|
+
let element: AXUIElement
|
|
639
|
+
if let accessibilityId = parsed.options["--id"] {
|
|
640
|
+
guard let found = findAccessibilityElement(in: appRoot, identifier: accessibilityId, label: nil) else {
|
|
641
|
+
throw NativeError.commandFailed("No element with id: \(accessibilityId)")
|
|
642
|
+
}
|
|
643
|
+
element = found
|
|
644
|
+
} else if let label = parsed.options["--label"] {
|
|
645
|
+
guard let found = findAccessibilityElement(in: appRoot, identifier: nil, label: label) else {
|
|
646
|
+
throw NativeError.commandFailed("No element with label: \(label)")
|
|
647
|
+
}
|
|
648
|
+
element = found
|
|
649
|
+
} else {
|
|
650
|
+
var focusedRef: CFTypeRef?
|
|
651
|
+
let status = AXUIElementCopyAttributeValue(appRoot, "AXFocusedUIElement" as CFString, &focusedRef)
|
|
652
|
+
guard status == .success, let ref = focusedRef else {
|
|
653
|
+
throw NativeError.commandFailed("No focused element and no --id or --label provided.")
|
|
654
|
+
}
|
|
655
|
+
element = ref as! AXUIElement
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 속성 이름 결정 (settable 체크와 set 호출 양쪽에 공통 사용)
|
|
659
|
+
let axAttributeName: String
|
|
660
|
+
switch attributeStr {
|
|
661
|
+
case "value": axAttributeName = kAXValueAttribute as String
|
|
662
|
+
case "selectedTextRange": axAttributeName = "AXSelectedTextRange"
|
|
663
|
+
case "focused": axAttributeName = kAXFocusedAttribute as String
|
|
664
|
+
default:
|
|
665
|
+
throw NativeError.invalidArguments("Unsupported attribute: \(attributeStr). Use: value, selectedTextRange, focused.")
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 쓰기 가능 여부 사전 확인
|
|
669
|
+
var settable: DarwinBoolean = false
|
|
670
|
+
AXUIElementIsAttributeSettable(element, axAttributeName as CFString, &settable)
|
|
671
|
+
if !settable.boolValue {
|
|
672
|
+
throw NativeError.commandFailed(
|
|
673
|
+
"Attribute '\(attributeStr)' is not writable on this element. "
|
|
674
|
+
+ "Use enumerate_ui to discover which attributes are settable."
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
switch attributeStr {
|
|
679
|
+
case "value":
|
|
680
|
+
let result = AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, valueStr as CFTypeRef)
|
|
681
|
+
guard result == .success else {
|
|
682
|
+
throw NativeError.commandFailed("Failed to set value (AXError \(result.rawValue)). Element may not support writable value.")
|
|
683
|
+
}
|
|
684
|
+
print("Set value: \(valueStr)")
|
|
685
|
+
|
|
686
|
+
case "selectedTextRange":
|
|
687
|
+
// Parse "location,length" format
|
|
688
|
+
let parts = valueStr.split(separator: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) }
|
|
689
|
+
guard parts.count == 2 else {
|
|
690
|
+
throw NativeError.invalidArguments("selectedTextRange requires 'location,length' format (e.g. '10,5').")
|
|
691
|
+
}
|
|
692
|
+
var range = CFRange(location: parts[0], length: parts[1])
|
|
693
|
+
guard let axValue = AXValueCreate(.cfRange, &range) else {
|
|
694
|
+
throw NativeError.commandFailed("Failed to create AXValue for range.")
|
|
695
|
+
}
|
|
696
|
+
let result = AXUIElementSetAttributeValue(element, "AXSelectedTextRange" as CFString, axValue)
|
|
697
|
+
guard result == .success else {
|
|
698
|
+
throw NativeError.commandFailed("Failed to set selectedTextRange (AXError \(result.rawValue)).")
|
|
699
|
+
}
|
|
700
|
+
print("Set selectedTextRange: location=\(parts[0]), length=\(parts[1])")
|
|
701
|
+
|
|
702
|
+
case "focused":
|
|
703
|
+
let boolValue = (valueStr == "true" || valueStr == "1")
|
|
704
|
+
let result = AXUIElementSetAttributeValue(element, kAXFocusedAttribute as CFString, boolValue as CFTypeRef)
|
|
705
|
+
guard result == .success else {
|
|
706
|
+
throw NativeError.commandFailed("Failed to set focused (AXError \(result.rawValue)).")
|
|
707
|
+
}
|
|
708
|
+
print("Set focused: \(boolValue)")
|
|
709
|
+
|
|
710
|
+
default:
|
|
711
|
+
throw NativeError.invalidArguments("Unsupported attribute: \(attributeStr). Use: value, selectedTextRange, focused.")
|
|
712
|
+
}
|
|
713
|
+
return 0
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// MARK: - handleHitTest (F003)
|
|
717
|
+
|
|
718
|
+
func handleHitTest(_ parsed: ParsedOptions) throws -> Int32 {
|
|
719
|
+
try ensureAccessibilityTrusted()
|
|
720
|
+
guard let xStr = parsed.options["-x"], let yStr = parsed.options["-y"],
|
|
721
|
+
let x = Float(xStr), let y = Float(yStr) else {
|
|
722
|
+
throw NativeError.invalidArguments("hit-test requires -x and -y coordinates.")
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
let systemWide = AXUIElementCreateSystemWide()
|
|
726
|
+
var element: AXUIElement?
|
|
727
|
+
let status = AXUIElementCopyElementAtPosition(systemWide, x, y, &element)
|
|
728
|
+
guard status == .success, let found = element else {
|
|
729
|
+
throw NativeError.commandFailed("No accessibility element found at (\(x), \(y)). AXError: \(status.rawValue)")
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let role = StringAttribute(found, kAXRoleAttribute as CFString) ?? ""
|
|
733
|
+
let subrole = StringAttribute(found, kAXSubroleAttribute as CFString) ?? ""
|
|
734
|
+
let title = StringAttribute(found, kAXTitleAttribute as CFString) ?? ""
|
|
735
|
+
let identifier = StringAttribute(found, "AXIdentifier" as CFString) ?? ""
|
|
736
|
+
let value = StringAttribute(found, kAXValueAttribute as CFString) ?? ""
|
|
737
|
+
let description = StringAttribute(found, kAXDescriptionAttribute as CFString) ?? ""
|
|
738
|
+
|
|
739
|
+
var frameStr = ""
|
|
740
|
+
if let frame = FrameAttribute(found) {
|
|
741
|
+
frameStr = "\(formatFloat(frame.origin.x)),\(formatFloat(frame.origin.y)),\(formatFloat(frame.width)),\(formatFloat(frame.height))"
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
var pid: pid_t = 0
|
|
745
|
+
AXUIElementGetPid(found, &pid)
|
|
746
|
+
let appName = NSWorkspace.shared.runningApplications.first(where: { $0.processIdentifier == pid })?.localizedName ?? ""
|
|
747
|
+
|
|
748
|
+
print("role=\(role) subrole=\(subrole) title=\(title) id=\(identifier) value=\(value) desc=\(description) frame=\(frameStr) pid=\(pid) app=\(appName)")
|
|
749
|
+
return 0
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// MARK: - handleEnumerateUI (F005)
|
|
753
|
+
|
|
754
|
+
func handleEnumerateUI(_ parsed: ParsedOptions) throws -> Int32 {
|
|
755
|
+
let target = try resolveTarget(from: parsed)
|
|
756
|
+
try ensureAccessibilityTrusted()
|
|
757
|
+
try activateTarget(target)
|
|
758
|
+
let appRoot = try accessibilityRootElement(for: target)
|
|
759
|
+
|
|
760
|
+
let element: AXUIElement
|
|
761
|
+
if let accessibilityId = parsed.options["--id"] {
|
|
762
|
+
guard let found = findAccessibilityElement(in: appRoot, identifier: accessibilityId, label: nil) else {
|
|
763
|
+
throw NativeError.commandFailed("No element with id: \(accessibilityId)")
|
|
764
|
+
}
|
|
765
|
+
element = found
|
|
766
|
+
} else if let label = parsed.options["--label"] {
|
|
767
|
+
guard let found = findAccessibilityElement(in: appRoot, identifier: nil, label: label) else {
|
|
768
|
+
throw NativeError.commandFailed("No element with label: \(label)")
|
|
769
|
+
}
|
|
770
|
+
element = found
|
|
771
|
+
} else {
|
|
772
|
+
var focusedRef: CFTypeRef?
|
|
773
|
+
let status = AXUIElementCopyAttributeValue(appRoot, "AXFocusedUIElement" as CFString, &focusedRef)
|
|
774
|
+
guard status == .success, let ref = focusedRef else {
|
|
775
|
+
throw NativeError.commandFailed("No focused element and no --id or --label provided.")
|
|
776
|
+
}
|
|
777
|
+
element = ref as! AXUIElement
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// 속성 목록
|
|
781
|
+
var attrNames: CFArray?
|
|
782
|
+
AXUIElementCopyAttributeNames(element, &attrNames)
|
|
783
|
+
var attributes: [(name: String, settable: Bool)] = []
|
|
784
|
+
if let names = attrNames as? [String] {
|
|
785
|
+
for name in names {
|
|
786
|
+
var settable: DarwinBoolean = false
|
|
787
|
+
AXUIElementIsAttributeSettable(element, name as CFString, &settable)
|
|
788
|
+
attributes.append((name: name, settable: settable.boolValue))
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// 액션 목록
|
|
793
|
+
var actionNames: CFArray?
|
|
794
|
+
AXUIElementCopyActionNames(element, &actionNames)
|
|
795
|
+
var actions: [(name: String, description: String)] = []
|
|
796
|
+
if let names = actionNames as? [String] {
|
|
797
|
+
for name in names {
|
|
798
|
+
var desc: CFString?
|
|
799
|
+
AXUIElementCopyActionDescription(element, name as CFString, &desc)
|
|
800
|
+
actions.append((name: name, description: (desc as String?) ?? ""))
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// 파라미터화된 속성 목록
|
|
805
|
+
var paramNames: CFArray?
|
|
806
|
+
AXUIElementCopyParameterizedAttributeNames(element, ¶mNames)
|
|
807
|
+
let parameterized = (paramNames as? [String]) ?? []
|
|
808
|
+
|
|
809
|
+
let attrJson = attributes.map { a in
|
|
810
|
+
let escapedName = a.name.replacingOccurrences(of: "\"", with: "\\\"")
|
|
811
|
+
return "{\"name\":\"\(escapedName)\",\"settable\":\(a.settable)}"
|
|
812
|
+
}.joined(separator: ",")
|
|
813
|
+
|
|
814
|
+
let actJson = actions.map { a in
|
|
815
|
+
let escapedName = a.name.replacingOccurrences(of: "\"", with: "\\\"")
|
|
816
|
+
let escapedDesc = a.description.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
|
|
817
|
+
return "{\"name\":\"\(escapedName)\",\"description\":\"\(escapedDesc)\"}"
|
|
818
|
+
}.joined(separator: ",")
|
|
819
|
+
|
|
820
|
+
let paramJson = parameterized.map { "\"" + $0.replacingOccurrences(of: "\"", with: "\\\"") + "\"" }.joined(separator: ",")
|
|
821
|
+
|
|
822
|
+
print("{\"attributes\":[\(attrJson)],\"actions\":[\(actJson)],\"parameterizedAttributes\":[\(paramJson)]}")
|
|
823
|
+
return 0
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// MARK: - handleReadUIParam (F012)
|
|
827
|
+
|
|
828
|
+
func handleReadUIParam(_ parsed: ParsedOptions) throws -> Int32 {
|
|
829
|
+
let target = try resolveTarget(from: parsed)
|
|
830
|
+
try ensureAccessibilityTrusted()
|
|
831
|
+
try activateTarget(target)
|
|
832
|
+
let appRoot = try accessibilityRootElement(for: target)
|
|
833
|
+
|
|
834
|
+
let attributeStr = try requiredOption("--attribute", from: parsed)
|
|
835
|
+
let paramStr = try requiredOption("--param", from: parsed)
|
|
836
|
+
|
|
837
|
+
// Find element
|
|
838
|
+
let element: AXUIElement
|
|
839
|
+
if let accessibilityId = parsed.options["--id"] {
|
|
840
|
+
guard let found = findAccessibilityElement(in: appRoot, identifier: accessibilityId, label: nil) else {
|
|
841
|
+
throw NativeError.commandFailed("No element with id: \(accessibilityId)")
|
|
842
|
+
}
|
|
843
|
+
element = found
|
|
844
|
+
} else if let label = parsed.options["--label"] {
|
|
845
|
+
guard let found = findAccessibilityElement(in: appRoot, identifier: nil, label: label) else {
|
|
846
|
+
throw NativeError.commandFailed("No element with label: \(label)")
|
|
847
|
+
}
|
|
848
|
+
element = found
|
|
849
|
+
} else {
|
|
850
|
+
var focusedRef: CFTypeRef?
|
|
851
|
+
let status = AXUIElementCopyAttributeValue(appRoot, "AXFocusedUIElement" as CFString, &focusedRef)
|
|
852
|
+
guard status == .success, let ref = focusedRef else {
|
|
853
|
+
throw NativeError.commandFailed("No focused element and no --id or --label provided.")
|
|
854
|
+
}
|
|
855
|
+
element = ref as! AXUIElement
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
let axAttribute: String
|
|
859
|
+
let parameter: CFTypeRef
|
|
860
|
+
|
|
861
|
+
switch attributeStr {
|
|
862
|
+
case "stringForRange", "boundsForRange":
|
|
863
|
+
let parts = paramStr.split(separator: ",").compactMap { Int($0.trimmingCharacters(in: .whitespaces)) }
|
|
864
|
+
guard parts.count == 2 else {
|
|
865
|
+
throw NativeError.invalidArguments("\(attributeStr) requires 'location,length' format.")
|
|
866
|
+
}
|
|
867
|
+
var range = CFRange(location: parts[0], length: parts[1])
|
|
868
|
+
guard let axValue = AXValueCreate(.cfRange, &range) else {
|
|
869
|
+
throw NativeError.commandFailed("Failed to create AXValue for range.")
|
|
870
|
+
}
|
|
871
|
+
parameter = axValue
|
|
872
|
+
axAttribute = attributeStr == "stringForRange"
|
|
873
|
+
? "AXStringForRange"
|
|
874
|
+
: "AXBoundsForRange"
|
|
875
|
+
|
|
876
|
+
case "lineForIndex":
|
|
877
|
+
guard let index = Int(paramStr) else {
|
|
878
|
+
throw NativeError.invalidArguments("lineForIndex requires a numeric index.")
|
|
879
|
+
}
|
|
880
|
+
parameter = index as CFTypeRef
|
|
881
|
+
axAttribute = "AXLineForIndex"
|
|
882
|
+
|
|
883
|
+
case "rangeForLine":
|
|
884
|
+
guard let line = Int(paramStr) else {
|
|
885
|
+
throw NativeError.invalidArguments("rangeForLine requires a numeric line number.")
|
|
886
|
+
}
|
|
887
|
+
parameter = line as CFTypeRef
|
|
888
|
+
axAttribute = "AXRangeForLine"
|
|
889
|
+
|
|
890
|
+
default:
|
|
891
|
+
throw NativeError.invalidArguments("Unsupported parameterized attribute: \(attributeStr). Use: stringForRange, boundsForRange, lineForIndex, rangeForLine.")
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
var resultRef: CFTypeRef?
|
|
895
|
+
let status = AXUIElementCopyParameterizedAttributeValue(element, axAttribute as CFString, parameter, &resultRef)
|
|
896
|
+
guard status == .success, let result = resultRef else {
|
|
897
|
+
throw NativeError.commandFailed("Failed to read \(attributeStr) (AXError \(status.rawValue)).")
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if let str = result as? String {
|
|
901
|
+
print(str)
|
|
902
|
+
} else if let num = result as? NSNumber {
|
|
903
|
+
print(num.stringValue)
|
|
904
|
+
} else if CFGetTypeID(result) == AXValueGetTypeID() {
|
|
905
|
+
let axVal = result as! AXValue
|
|
906
|
+
let valType = AXValueGetType(axVal)
|
|
907
|
+
if valType == .cfRange {
|
|
908
|
+
var range = CFRange(location: 0, length: 0)
|
|
909
|
+
AXValueGetValue(axVal, .cfRange, &range)
|
|
910
|
+
print("\(range.location),\(range.length)")
|
|
911
|
+
} else if valType == .cgRect {
|
|
912
|
+
var rect = CGRect.zero
|
|
913
|
+
AXValueGetValue(axVal, .cgRect, &rect)
|
|
914
|
+
print("\(rect.origin.x),\(rect.origin.y),\(rect.width),\(rect.height)")
|
|
915
|
+
} else if valType == .cgPoint {
|
|
916
|
+
var point = CGPoint.zero
|
|
917
|
+
AXValueGetValue(axVal, .cgPoint, &point)
|
|
918
|
+
print("\(point.x),\(point.y)")
|
|
919
|
+
} else {
|
|
920
|
+
print(String(describing: result))
|
|
921
|
+
}
|
|
922
|
+
} else {
|
|
923
|
+
print(String(describing: result))
|
|
924
|
+
}
|
|
925
|
+
return 0
|
|
926
|
+
}
|
|
@@ -36,6 +36,17 @@ let supportedCommands: Set<String> = [
|
|
|
36
36
|
"menu-action",
|
|
37
37
|
"get-focused-app",
|
|
38
38
|
"clipboard",
|
|
39
|
+
"list-input-sources",
|
|
40
|
+
"input-source",
|
|
41
|
+
"focus-window",
|
|
42
|
+
"read-ui-value",
|
|
43
|
+
"context-menu-action",
|
|
44
|
+
"detect-dialog",
|
|
45
|
+
"set-ui-value",
|
|
46
|
+
"read-ui-param",
|
|
47
|
+
"hit-test",
|
|
48
|
+
"enumerate-ui",
|
|
49
|
+
"watch-notification",
|
|
39
50
|
]
|
|
40
51
|
|
|
41
52
|
// MARK: - Argument Parsing
|
|
@@ -291,7 +302,13 @@ func accessibilityRootElement(for target: TargetApp) throws -> UIElement {
|
|
|
291
302
|
case .simulator:
|
|
292
303
|
return try simulatorAccessibilityRootElement()
|
|
293
304
|
case .macApp(let pid, _, _):
|
|
294
|
-
|
|
305
|
+
let appElement = AXUIElementCreateApplication(pid)
|
|
306
|
+
// 무거운 앱에서 기본 시스템 타임아웃(~6초)이 너무 짧은 경우 대비
|
|
307
|
+
// BAEPSAE_AX_TIMEOUT 환경변수(초)로 재정의 가능, 기본값 10초
|
|
308
|
+
let timeout = Float(ProcessInfo.processInfo.environment["BAEPSAE_AX_TIMEOUT"]
|
|
309
|
+
.flatMap(Double.init) ?? 10.0)
|
|
310
|
+
AXUIElementSetMessagingTimeout(appElement, timeout)
|
|
311
|
+
return appElement
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
314
|
|
|
@@ -1482,9 +1499,17 @@ func activateTarget(_ target: TargetApp) throws {
|
|
|
1482
1499
|
try activateSimulator(udid: udid)
|
|
1483
1500
|
case .macApp(let pid, _, _):
|
|
1484
1501
|
let apps = NSWorkspace.shared.runningApplications.filter { $0.processIdentifier == pid }
|
|
1485
|
-
|
|
1486
|
-
|
|
1502
|
+
guard let app = apps.first else {
|
|
1503
|
+
throw NativeError.commandFailed("App with pid \(pid) not found.")
|
|
1504
|
+
}
|
|
1505
|
+
app.activate(options: [.activateAllWindows])
|
|
1506
|
+
// Poll for activation (max 1 second)
|
|
1507
|
+
let deadline = Date().addingTimeInterval(1.0)
|
|
1508
|
+
while Date() < deadline {
|
|
1509
|
+
if app.isActive { return }
|
|
1510
|
+
Thread.sleep(forTimeInterval: 0.05)
|
|
1487
1511
|
}
|
|
1512
|
+
fputs("Warning: app activation may not have completed within timeout\n", stderr)
|
|
1488
1513
|
}
|
|
1489
1514
|
}
|
|
1490
1515
|
|
|
@@ -1520,6 +1545,7 @@ func postMouseEvent(type: CGEventType, point: CGPoint, button: CGMouseButton = .
|
|
|
1520
1545
|
|
|
1521
1546
|
func sendClick(at point: CGPoint) {
|
|
1522
1547
|
postMouseEvent(type: .leftMouseDown, point: point)
|
|
1548
|
+
usleep(20_000) // 20ms prevents some apps from ignoring instant clicks
|
|
1523
1549
|
postMouseEvent(type: .leftMouseUp, point: point)
|
|
1524
1550
|
}
|
|
1525
1551
|
|
|
@@ -1607,6 +1633,21 @@ func sendDrag(from start: CGPoint, to end: CGPoint, holdDuration: Double, moveDu
|
|
|
1607
1633
|
postMouseEvent(type: .leftMouseUp, point: end)
|
|
1608
1634
|
}
|
|
1609
1635
|
|
|
1636
|
+
// MARK: - Keycode to CGEventFlags Mapping
|
|
1637
|
+
|
|
1638
|
+
let keycodeToFlag: [Int: CGEventFlags] = [
|
|
1639
|
+
0x37: .maskCommand, // Left Command (55)
|
|
1640
|
+
0x36: .maskCommand, // Right Command (54)
|
|
1641
|
+
0x38: .maskShift, // Left Shift (56)
|
|
1642
|
+
0x3C: .maskShift, // Right Shift (60)
|
|
1643
|
+
0x3A: .maskAlternate, // Left Option (58)
|
|
1644
|
+
0x3D: .maskAlternate, // Right Option (61)
|
|
1645
|
+
0x3B: .maskControl, // Left Control (59)
|
|
1646
|
+
0x3E: .maskControl, // Right Control (62)
|
|
1647
|
+
0x39: .maskAlphaShift, // Caps Lock (57)
|
|
1648
|
+
0x3F: .maskSecondaryFn, // Fn (63)
|
|
1649
|
+
]
|
|
1650
|
+
|
|
1610
1651
|
// MARK: - Keyboard Events
|
|
1611
1652
|
|
|
1612
1653
|
func sendKeyPress(keyCode: Int, duration: Double?) {
|
|
@@ -1622,34 +1663,54 @@ func sendKeyPress(keyCode: Int, duration: Double?) {
|
|
|
1622
1663
|
|
|
1623
1664
|
func sendKeyCombo(modifiers: [Int], key: Int) {
|
|
1624
1665
|
let source = CGEventSource(stateID: .hidSystemState)
|
|
1666
|
+
|
|
1667
|
+
// Convert keycodes to CGEventFlags
|
|
1668
|
+
var flags: CGEventFlags = []
|
|
1669
|
+
for modifier in modifiers {
|
|
1670
|
+
if let flag = keycodeToFlag[modifier] {
|
|
1671
|
+
flags.insert(flag)
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// Send modifier key-down events with flags (backward compat with apps that watch key events)
|
|
1625
1676
|
for modifier in modifiers {
|
|
1626
1677
|
let event = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(modifier), keyDown: true)
|
|
1678
|
+
event?.flags = flags
|
|
1627
1679
|
event?.post(tap: .cghidEventTap)
|
|
1628
1680
|
}
|
|
1681
|
+
usleep(30_000) // 30ms for modifiers to register
|
|
1682
|
+
|
|
1683
|
+
// Ensure modifier key-up always happens (prevent stuck modifiers)
|
|
1684
|
+
defer {
|
|
1685
|
+
usleep(30_000)
|
|
1686
|
+
for modifier in modifiers.reversed() {
|
|
1687
|
+
let event = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(modifier), keyDown: false)
|
|
1688
|
+
event?.post(tap: .cghidEventTap)
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
// Main key with flags set (critical: many apps only check flags, not key events)
|
|
1629
1693
|
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(key), keyDown: true)
|
|
1630
|
-
|
|
1694
|
+
keyDown?.flags = flags
|
|
1631
1695
|
keyDown?.post(tap: .cghidEventTap)
|
|
1696
|
+
|
|
1697
|
+
let keyUp = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(key), keyDown: false)
|
|
1698
|
+
keyUp?.flags = flags
|
|
1632
1699
|
keyUp?.post(tap: .cghidEventTap)
|
|
1633
|
-
for modifier in modifiers.reversed() {
|
|
1634
|
-
let event = CGEvent(keyboardEventSource: source, virtualKey: CGKeyCode(modifier), keyDown: false)
|
|
1635
|
-
event?.post(tap: .cghidEventTap)
|
|
1636
|
-
}
|
|
1637
1700
|
}
|
|
1638
1701
|
|
|
1639
|
-
func sendText(_ text: String) {
|
|
1702
|
+
func sendText(_ text: String, charDelay: Double = 0.01) {
|
|
1640
1703
|
let source = CGEventSource(stateID: .hidSystemState)
|
|
1641
|
-
for
|
|
1642
|
-
|
|
1643
|
-
guard value <= UInt16.max else {
|
|
1644
|
-
continue
|
|
1645
|
-
}
|
|
1646
|
-
var char = UniChar(value)
|
|
1704
|
+
for char in text {
|
|
1705
|
+
var utf16Chars = Array(char.utf16)
|
|
1647
1706
|
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: true)
|
|
1648
|
-
keyDown?.keyboardSetUnicodeString(stringLength:
|
|
1707
|
+
keyDown?.keyboardSetUnicodeString(stringLength: utf16Chars.count, unicodeString: &utf16Chars)
|
|
1649
1708
|
keyDown?.post(tap: .cghidEventTap)
|
|
1650
1709
|
let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: false)
|
|
1651
|
-
keyUp?.keyboardSetUnicodeString(stringLength: 1, unicodeString: &char)
|
|
1652
1710
|
keyUp?.post(tap: .cghidEventTap)
|
|
1711
|
+
if charDelay > 0 {
|
|
1712
|
+
Thread.sleep(forTimeInterval: charDelay)
|
|
1713
|
+
}
|
|
1653
1714
|
}
|
|
1654
1715
|
}
|
|
1655
1716
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
let BAEPSAE_VERSION = "6.
|
|
1
|
+
let BAEPSAE_VERSION = "6.3.0"
|