@anionex/dsh-computer-use 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +307 -0
  4. package/README.zh.md +307 -0
  5. package/assets/computer-use-fixture.png +0 -0
  6. package/assets/cursor.png +0 -0
  7. package/cordis.patch.yml +13 -0
  8. package/docs/interaction-policy.i18n.yaml +4 -0
  9. package/docs/interaction-policy.md +140 -0
  10. package/docs/interaction-policy.zh.md +140 -0
  11. package/lib/approval-policy.js +12 -0
  12. package/lib/approval-policy.js.map +1 -0
  13. package/lib/artifacts.js +79 -0
  14. package/lib/artifacts.js.map +1 -0
  15. package/lib/backend.js +3 -0
  16. package/lib/backend.js.map +1 -0
  17. package/lib/client.js +349 -0
  18. package/lib/client.js.map +1 -0
  19. package/lib/config.js +120 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/confirmations.js +99 -0
  22. package/lib/confirmations.js.map +1 -0
  23. package/lib/diff.js +58 -0
  24. package/lib/diff.js.map +1 -0
  25. package/lib/errors.js +22 -0
  26. package/lib/errors.js.map +1 -0
  27. package/lib/exposure.js +231 -0
  28. package/lib/exposure.js.map +1 -0
  29. package/lib/index.js +56 -0
  30. package/lib/index.js.map +1 -0
  31. package/lib/leases.js +259 -0
  32. package/lib/leases.js.map +1 -0
  33. package/lib/providers/macos.js +148 -0
  34. package/lib/providers/macos.js.map +1 -0
  35. package/lib/providers/native-helper.js +356 -0
  36. package/lib/providers/native-helper.js.map +1 -0
  37. package/lib/service.js +480 -0
  38. package/lib/service.js.map +1 -0
  39. package/lib/skill.js +119 -0
  40. package/lib/skill.js.map +1 -0
  41. package/lib/target-resolver.js +180 -0
  42. package/lib/target-resolver.js.map +1 -0
  43. package/lib/tools.js +493 -0
  44. package/lib/tools.js.map +1 -0
  45. package/lib/types/approval-policy.d.ts +13 -0
  46. package/lib/types/approval-policy.d.ts.map +1 -0
  47. package/lib/types/artifacts.d.ts +10 -0
  48. package/lib/types/artifacts.d.ts.map +1 -0
  49. package/lib/types/backend.d.ts +94 -0
  50. package/lib/types/backend.d.ts.map +1 -0
  51. package/lib/types/client/index.d.ts +78 -0
  52. package/lib/types/client/index.d.ts.map +1 -0
  53. package/lib/types/config.d.ts +75 -0
  54. package/lib/types/config.d.ts.map +1 -0
  55. package/lib/types/confirmations.d.ts +23 -0
  56. package/lib/types/confirmations.d.ts.map +1 -0
  57. package/lib/types/diff.d.ts +5 -0
  58. package/lib/types/diff.d.ts.map +1 -0
  59. package/lib/types/errors.d.ts +15 -0
  60. package/lib/types/errors.d.ts.map +1 -0
  61. package/lib/types/exposure.d.ts +30 -0
  62. package/lib/types/exposure.d.ts.map +1 -0
  63. package/lib/types/index.d.ts +21 -0
  64. package/lib/types/index.d.ts.map +1 -0
  65. package/lib/types/leases.d.ts +80 -0
  66. package/lib/types/leases.d.ts.map +1 -0
  67. package/lib/types/providers/macos.d.ts +15 -0
  68. package/lib/types/providers/macos.d.ts.map +1 -0
  69. package/lib/types/providers/native-helper.d.ts +36 -0
  70. package/lib/types/providers/native-helper.d.ts.map +1 -0
  71. package/lib/types/service.d.ts +52 -0
  72. package/lib/types/service.d.ts.map +1 -0
  73. package/lib/types/skill.d.ts +9 -0
  74. package/lib/types/skill.d.ts.map +1 -0
  75. package/lib/types/target-resolver.d.ts +38 -0
  76. package/lib/types/target-resolver.d.ts.map +1 -0
  77. package/lib/types/tools.d.ts +6 -0
  78. package/lib/types/tools.d.ts.map +1 -0
  79. package/lib/types/types.d.ts +246 -0
  80. package/lib/types/types.d.ts.map +1 -0
  81. package/lib/types/web.d.ts +32 -0
  82. package/lib/types/web.d.ts.map +1 -0
  83. package/lib/types.js +9 -0
  84. package/lib/types.js.map +1 -0
  85. package/lib/web.js +163 -0
  86. package/lib/web.js.map +1 -0
  87. package/native/macos/Sources/Fixture/main.swift +366 -0
  88. package/native/macos/Sources/Helper/CursorImage.swift +11 -0
  89. package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
  90. package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
  91. package/native/macos/Sources/Helper/main.swift +1140 -0
  92. package/native/macos/Sources/Monitor/main.swift +139 -0
  93. package/native/macos/bin/dsh-computer-use-helper +0 -0
  94. package/native/macos/manifest.json +14 -0
  95. package/package.json +128 -0
  96. package/scripts/build-client.mjs +24 -0
  97. package/scripts/build-native.mjs +158 -0
  98. package/scripts/check-native.mjs +92 -0
  99. package/scripts/model-e2e.mjs +291 -0
  100. package/scripts/session-transcript.mjs +117 -0
  101. package/scripts/validate.mjs +256 -0
  102. package/src/approval-policy.ts +16 -0
  103. package/src/artifacts.ts +93 -0
  104. package/src/backend.ts +103 -0
  105. package/src/client/index.tsx +539 -0
  106. package/src/config.ts +196 -0
  107. package/src/confirmations.ts +128 -0
  108. package/src/diff.ts +52 -0
  109. package/src/errors.ts +39 -0
  110. package/src/exposure.ts +236 -0
  111. package/src/index.ts +66 -0
  112. package/src/leases.ts +337 -0
  113. package/src/providers/macos.ts +185 -0
  114. package/src/providers/native-helper.ts +393 -0
  115. package/src/service.ts +583 -0
  116. package/src/skill.ts +123 -0
  117. package/src/target-resolver.ts +237 -0
  118. package/src/tools.ts +528 -0
  119. package/src/types.ts +299 -0
  120. package/src/web.ts +180 -0
@@ -0,0 +1,488 @@
1
+ import AppKit
2
+ import CoreGraphics
3
+ import Darwin
4
+ import Foundation
5
+ import QuartzCore
6
+
7
+ private struct CursorOverlayCommand {
8
+ let operation: String
9
+ let point: CGPoint?
10
+ let durationMs: Int
11
+ let autoHideMs: Int
12
+ let targetPid: pid_t?
13
+ let targetWindowNumber: Int64?
14
+ let targetWindowFrame: CGRect?
15
+ let sustainedPress: Bool
16
+
17
+ private static let maximumCoordinateMagnitude = 100_000.0
18
+ private static let maximumDurationMs = 2_000
19
+ private static let maximumAutoHideMs = 30_000
20
+
21
+ private static func number(_ object: Any?, field: String) throws -> NSNumber? {
22
+ guard let object else { return nil }
23
+ guard let value = object as? NSNumber,
24
+ CFGetTypeID(value) != CFBooleanGetTypeID() else {
25
+ throw CursorOverlayError(message: "cursor overlay \(field) must be numeric")
26
+ }
27
+ return value
28
+ }
29
+
30
+ private static func integer(_ object: Any?, field: String, fallback: Int) throws -> Int {
31
+ guard let value = try number(object, field: field) else { return fallback }
32
+ let doubleValue = value.doubleValue
33
+ guard doubleValue.isFinite, doubleValue.rounded(.towardZero) == doubleValue,
34
+ doubleValue >= Double(Int.min), doubleValue <= Double(Int.max) else {
35
+ throw CursorOverlayError(message: "cursor overlay \(field) must be an integer")
36
+ }
37
+ return Int(doubleValue)
38
+ }
39
+
40
+ init(_ object: [String: Any]) throws {
41
+ guard let operation = object["op"] as? String else {
42
+ throw CursorOverlayError(message: "cursor overlay command is missing op")
43
+ }
44
+ self.operation = operation
45
+ let x = try Self.number(object["x"], field: "x")
46
+ let y = try Self.number(object["y"], field: "y")
47
+ if let x, let y {
48
+ let xValue = x.doubleValue
49
+ let yValue = y.doubleValue
50
+ guard xValue.isFinite, yValue.isFinite,
51
+ abs(xValue) <= Self.maximumCoordinateMagnitude,
52
+ abs(yValue) <= Self.maximumCoordinateMagnitude else {
53
+ throw CursorOverlayError(message: "cursor overlay coordinates are outside the supported range")
54
+ }
55
+ self.point = CGPoint(x: xValue, y: yValue)
56
+ } else {
57
+ self.point = nil
58
+ }
59
+ let durationMs = try Self.integer(object["durationMs"], field: "durationMs", fallback: 180)
60
+ let autoHideMs = try Self.integer(object["autoHideMs"], field: "autoHideMs", fallback: 0)
61
+ guard durationMs >= 0, durationMs <= Self.maximumDurationMs,
62
+ autoHideMs >= 0, autoHideMs <= Self.maximumAutoHideMs else {
63
+ throw CursorOverlayError(message: "cursor overlay timing is outside the supported range")
64
+ }
65
+ self.durationMs = durationMs
66
+ self.autoHideMs = autoHideMs
67
+
68
+ if let rawPid = try Self.number(object["targetPid"], field: "targetPid") {
69
+ let pidValue = rawPid.doubleValue
70
+ guard pidValue.isFinite, pidValue.rounded(.towardZero) == pidValue,
71
+ pidValue > 0, pidValue <= Double(Int32.max) else {
72
+ throw CursorOverlayError(message: "cursor overlay targetPid is invalid")
73
+ }
74
+ self.targetPid = pid_t(pidValue)
75
+ } else {
76
+ self.targetPid = nil
77
+ }
78
+ if let rawWindowNumber = try Self.number(object["targetWindowNumber"], field: "targetWindowNumber") {
79
+ let windowValue = rawWindowNumber.doubleValue
80
+ guard windowValue.isFinite, windowValue.rounded(.towardZero) == windowValue,
81
+ windowValue > 0, windowValue <= Double(Int64.max) else {
82
+ throw CursorOverlayError(message: "cursor overlay targetWindowNumber is invalid")
83
+ }
84
+ self.targetWindowNumber = Int64(windowValue)
85
+ } else {
86
+ self.targetWindowNumber = nil
87
+ }
88
+ if let frame = object["targetWindowFrame"] as? [String: Any] {
89
+ guard let x = try Self.number(frame["x"], field: "targetWindowFrame.x"),
90
+ let y = try Self.number(frame["y"], field: "targetWindowFrame.y"),
91
+ let width = try Self.number(frame["width"], field: "targetWindowFrame.width"),
92
+ let height = try Self.number(frame["height"], field: "targetWindowFrame.height") else {
93
+ throw CursorOverlayError(message: "cursor overlay targetWindowFrame is incomplete")
94
+ }
95
+ let values = [x.doubleValue, y.doubleValue, width.doubleValue, height.doubleValue]
96
+ guard values.allSatisfy(\.isFinite), width.doubleValue > 0, height.doubleValue > 0,
97
+ values.allSatisfy({ abs($0) <= Self.maximumCoordinateMagnitude }) else {
98
+ throw CursorOverlayError(message: "cursor overlay targetWindowFrame is invalid")
99
+ }
100
+ self.targetWindowFrame = CGRect(
101
+ x: x.doubleValue,
102
+ y: y.doubleValue,
103
+ width: width.doubleValue,
104
+ height: height.doubleValue
105
+ )
106
+ } else {
107
+ self.targetWindowFrame = nil
108
+ }
109
+ if let sustainedPress = object["sustainedPress"] {
110
+ guard let value = sustainedPress as? Bool else {
111
+ throw CursorOverlayError(message: "cursor overlay sustainedPress must be boolean")
112
+ }
113
+ self.sustainedPress = value
114
+ } else {
115
+ self.sustainedPress = false
116
+ }
117
+ }
118
+ }
119
+
120
+ private struct CursorOverlayError: Error {
121
+ let message: String
122
+ }
123
+
124
+ private final class CursorPanel: NSPanel {
125
+ override var canBecomeKey: Bool { false }
126
+ override var canBecomeMain: Bool { false }
127
+ }
128
+
129
+ private final class CursorView: NSView {
130
+ override var isFlipped: Bool { true }
131
+
132
+ private let image: NSImage? = EmbeddedCursorImage.image
133
+
134
+ var pressed = false {
135
+ didSet { needsDisplay = true }
136
+ }
137
+
138
+ override var isOpaque: Bool { false }
139
+
140
+ override func draw(_ dirtyRect: NSRect) {
141
+ super.draw(dirtyRect)
142
+ guard let image else { return }
143
+ let imageRect = pressed ? bounds.insetBy(dx: 2, dy: 2) : bounds
144
+ image.draw(
145
+ in: imageRect,
146
+ from: .zero,
147
+ operation: .sourceOver,
148
+ fraction: pressed ? 0.85 : 1,
149
+ respectFlipped: true,
150
+ hints: nil
151
+ )
152
+ }
153
+ }
154
+
155
+ @MainActor
156
+ private final class CursorOverlayController: NSObject {
157
+ private static let size = NSSize(width: 28, height: 28)
158
+
159
+ private let window: CursorPanel
160
+ private let cursorView: CursorView
161
+ private var hideWork: DispatchWorkItem?
162
+ private var releaseWork: DispatchWorkItem?
163
+ private var targetCheckTimer: Timer?
164
+ private var targetPid: pid_t?
165
+ private var targetWindowNumber: Int64?
166
+ private var targetWindowFrame: CGRect?
167
+
168
+ override init() {
169
+ cursorView = CursorView(frame: NSRect(origin: .zero, size: Self.size))
170
+ window = CursorPanel(
171
+ contentRect: NSRect(origin: .zero, size: Self.size),
172
+ styleMask: [.borderless, .nonactivatingPanel],
173
+ backing: .buffered,
174
+ defer: false
175
+ )
176
+ super.init()
177
+ window.title = "DSH Computer Use Cursor"
178
+ window.contentView = cursorView
179
+ window.isOpaque = false
180
+ window.backgroundColor = .clear
181
+ window.hasShadow = false
182
+ window.ignoresMouseEvents = true
183
+ window.hidesOnDeactivate = false
184
+ window.becomesKeyOnlyIfNeeded = true
185
+ window.level = .floating
186
+ window.collectionBehavior = [.fullScreenAuxiliary, .stationary, .ignoresCycle]
187
+ window.isReleasedWhenClosed = false
188
+ window.sharingType = .readOnly
189
+ }
190
+
191
+ func show(
192
+ at quartzPoint: CGPoint,
193
+ durationMs: Int,
194
+ autoHideMs: Int,
195
+ targetPid: pid_t?,
196
+ targetWindowNumber: Int64?,
197
+ targetWindowFrame: CGRect?
198
+ ) {
199
+ hideWork?.cancel()
200
+ guard targetWindowIsCurrent(pid: targetPid, windowNumber: targetWindowNumber, expectedFrame: targetWindowFrame) else {
201
+ hide()
202
+ return
203
+ }
204
+ self.targetPid = targetPid
205
+ self.targetWindowNumber = targetWindowNumber
206
+ self.targetWindowFrame = targetWindowFrame
207
+ scheduleTargetChecks()
208
+ let point = appKitPoint(fromQuartz: quartzPoint)
209
+ let targetOrigin = NSPoint(
210
+ x: point.x,
211
+ y: point.y - Self.size.height
212
+ )
213
+ if window.isVisible && durationMs > 0 {
214
+ NSAnimationContext.runAnimationGroup { context in
215
+ context.duration = Double(durationMs) / 1000
216
+ context.timingFunction = CAMediaTimingFunction(name: .easeOut)
217
+ window.animator().setFrameOrigin(targetOrigin)
218
+ }
219
+ } else {
220
+ window.setFrameOrigin(targetOrigin)
221
+ window.orderFrontRegardless()
222
+ }
223
+ scheduleHide(after: autoHideMs)
224
+ }
225
+
226
+ func press(autoHideMs: Int, sustained: Bool) {
227
+ guard window.isVisible else { return }
228
+ releaseWork?.cancel()
229
+ releaseWork = nil
230
+ cursorView.pressed = true
231
+ if !sustained {
232
+ let work = DispatchWorkItem { [weak self] in
233
+ self?.cursorView.pressed = false
234
+ }
235
+ releaseWork = work
236
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150), execute: work)
237
+ }
238
+ scheduleHide(after: autoHideMs)
239
+ }
240
+
241
+ func release(autoHideMs: Int) {
242
+ releaseWork?.cancel()
243
+ releaseWork = nil
244
+ cursorView.pressed = false
245
+ scheduleHide(after: autoHideMs)
246
+ }
247
+
248
+ func validateTarget(pid: pid_t?, windowNumber: Int64?, expectedFrame: CGRect?) {
249
+ guard window.isVisible else { return }
250
+ if !targetWindowIsCurrent(pid: pid, windowNumber: windowNumber, expectedFrame: expectedFrame) { hide() }
251
+ }
252
+
253
+ func hide() {
254
+ hideWork?.cancel()
255
+ hideWork = nil
256
+ releaseWork?.cancel()
257
+ releaseWork = nil
258
+ targetCheckTimer?.invalidate()
259
+ targetCheckTimer = nil
260
+ targetPid = nil
261
+ targetWindowNumber = nil
262
+ targetWindowFrame = nil
263
+ cursorView.pressed = false
264
+ window.orderOut(nil)
265
+ }
266
+
267
+ func stop() {
268
+ hide()
269
+ NSApp.stop(nil)
270
+ if let wakeEvent = NSEvent.otherEvent(
271
+ with: .applicationDefined,
272
+ location: .zero,
273
+ modifierFlags: [],
274
+ timestamp: ProcessInfo.processInfo.systemUptime,
275
+ windowNumber: 0,
276
+ context: nil,
277
+ subtype: 0,
278
+ data1: 0,
279
+ data2: 0
280
+ ) {
281
+ NSApp.postEvent(wakeEvent, atStart: true)
282
+ }
283
+ }
284
+
285
+ private func scheduleHide(after milliseconds: Int) {
286
+ hideWork?.cancel()
287
+ guard milliseconds > 0 else { return }
288
+ let work = DispatchWorkItem { [weak self] in self?.hide() }
289
+ hideWork = work
290
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(milliseconds), execute: work)
291
+ }
292
+
293
+ private func scheduleTargetChecks() {
294
+ guard targetCheckTimer == nil else { return }
295
+ targetCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
296
+ Task { @MainActor [weak self] in
297
+ guard let self, self.window.isVisible else { return }
298
+ if !self.targetWindowIsCurrent(
299
+ pid: self.targetPid,
300
+ windowNumber: self.targetWindowNumber,
301
+ expectedFrame: self.targetWindowFrame
302
+ ) {
303
+ self.hide()
304
+ }
305
+ }
306
+ }
307
+ }
308
+
309
+ private func appKitPoint(fromQuartz point: CGPoint) -> NSPoint {
310
+ for screen in NSScreen.screens {
311
+ guard let screenNumber = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value else {
312
+ continue
313
+ }
314
+ let quartzFrame = CGDisplayBounds(CGDirectDisplayID(screenNumber))
315
+ if quartzFrame.contains(point) {
316
+ return NSPoint(
317
+ x: screen.frame.origin.x + (point.x - quartzFrame.origin.x),
318
+ y: screen.frame.maxY - (point.y - quartzFrame.origin.y)
319
+ )
320
+ }
321
+ }
322
+ let mainDisplay = CGMainDisplayID()
323
+ let quartzFrame = CGDisplayBounds(mainDisplay)
324
+ let screen = NSScreen.screens.first { candidate in
325
+ (candidate.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value == mainDisplay
326
+ } ?? NSScreen.main
327
+ let appKitFrame = screen?.frame ?? .zero
328
+ return NSPoint(
329
+ x: appKitFrame.origin.x + (point.x - quartzFrame.origin.x),
330
+ y: appKitFrame.maxY - (point.y - quartzFrame.origin.y)
331
+ )
332
+ }
333
+
334
+ private func targetWindowIsCurrent(pid: pid_t?, windowNumber: Int64?, expectedFrame: CGRect?) -> Bool {
335
+ guard let pid, let windowNumber, let expectedFrame else { return false }
336
+ guard let rawWindows = CGWindowListCopyWindowInfo(
337
+ [.optionOnScreenOnly, .excludeDesktopElements],
338
+ kCGNullWindowID
339
+ ) as? [[String: Any]] else {
340
+ return false
341
+ }
342
+ return rawWindows.contains { window in
343
+ guard (window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == pid,
344
+ (window[kCGWindowNumber as String] as? NSNumber)?.int64Value == windowNumber,
345
+ let bounds = window[kCGWindowBounds as String] as? [String: Any],
346
+ let currentFrame = CGRect(dictionaryRepresentation: bounds as CFDictionary) else { return false }
347
+ let tolerance: CGFloat = 2
348
+ return abs(currentFrame.minX - expectedFrame.minX) <= tolerance
349
+ && abs(currentFrame.minY - expectedFrame.minY) <= tolerance
350
+ && abs(currentFrame.width - expectedFrame.width) <= tolerance
351
+ && abs(currentFrame.height - expectedFrame.height) <= tolerance
352
+ }
353
+ }
354
+ }
355
+
356
+ @MainActor
357
+ enum CursorOverlayRuntime {
358
+ static func run() {
359
+ let app = NSApplication.shared
360
+ app.setActivationPolicy(.prohibited)
361
+ let controller = CursorOverlayController()
362
+ let input = FileHandle.standardInput
363
+ let parser = CursorCommandParser { object in
364
+ DispatchQueue.main.async {
365
+ handleCursorCommand(object, controller: controller)
366
+ }
367
+ } onEnd: {
368
+ DispatchQueue.main.async { controller.stop() }
369
+ } onInvalid: { message in
370
+ DispatchQueue.main.async {
371
+ emitCursorResponse(["ok": false, "error": message])
372
+ controller.stop()
373
+ }
374
+ }
375
+ input.readabilityHandler = { handle in parser.consume(handle.availableData) }
376
+ emitCursorResponse(["ok": true, "ready": true, "pid": ProcessInfo.processInfo.processIdentifier])
377
+ app.run()
378
+ input.readabilityHandler = nil
379
+ }
380
+ }
381
+
382
+ private final class CursorCommandParser: @unchecked Sendable {
383
+ private static let maximumLineBytes = 16 * 1024
384
+ private let queue = DispatchQueue(label: "dsh-computer-use.cursor-protocol")
385
+ private let onObject: @Sendable ([String: Any]) -> Void
386
+ private let onEnd: @Sendable () -> Void
387
+ private let onInvalid: @Sendable (String) -> Void
388
+ private var buffer = Data()
389
+ private var ended = false
390
+
391
+ init(
392
+ onObject: @escaping @Sendable ([String: Any]) -> Void,
393
+ onEnd: @escaping @Sendable () -> Void,
394
+ onInvalid: @escaping @Sendable (String) -> Void
395
+ ) {
396
+ self.onObject = onObject
397
+ self.onEnd = onEnd
398
+ self.onInvalid = onInvalid
399
+ }
400
+
401
+ func consume(_ data: Data) {
402
+ queue.async { [self] in
403
+ guard !ended, !data.isEmpty else {
404
+ guard !ended else { return }
405
+ ended = true
406
+ onEnd()
407
+ return
408
+ }
409
+ buffer.append(data)
410
+ if buffer.count > Self.maximumLineBytes, buffer.firstIndex(of: 0x0a) == nil {
411
+ ended = true
412
+ buffer.removeAll()
413
+ onInvalid("cursor overlay command exceeded the protocol limit")
414
+ return
415
+ }
416
+ while let newline = buffer.firstIndex(of: 0x0a) {
417
+ let line = buffer[..<newline]
418
+ buffer.removeSubrange(...newline)
419
+ if line.count > Self.maximumLineBytes {
420
+ ended = true
421
+ onInvalid("cursor overlay command exceeded the protocol limit")
422
+ return
423
+ }
424
+ guard !line.isEmpty else { continue }
425
+ guard let object = try? JSONSerialization.jsonObject(with: Data(line)) as? [String: Any] else {
426
+ ended = true
427
+ onInvalid("cursor overlay command is not valid JSON")
428
+ return
429
+ }
430
+ onObject(object)
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ @MainActor
437
+ private func handleCursorCommand(_ object: [String: Any], controller: CursorOverlayController) {
438
+ do {
439
+ let command = try CursorOverlayCommand(object)
440
+ switch command.operation {
441
+ case "show", "move":
442
+ guard let point = command.point else {
443
+ throw CursorOverlayError(message: "cursor overlay command needs x and y")
444
+ }
445
+ controller.show(
446
+ at: point,
447
+ durationMs: command.durationMs,
448
+ autoHideMs: command.autoHideMs,
449
+ targetPid: command.targetPid,
450
+ targetWindowNumber: command.targetWindowNumber,
451
+ targetWindowFrame: command.targetWindowFrame
452
+ )
453
+ case "press":
454
+ controller.validateTarget(
455
+ pid: command.targetPid,
456
+ windowNumber: command.targetWindowNumber,
457
+ expectedFrame: command.targetWindowFrame
458
+ )
459
+ controller.press(autoHideMs: command.autoHideMs, sustained: command.sustainedPress)
460
+ case "release":
461
+ controller.validateTarget(
462
+ pid: command.targetPid,
463
+ windowNumber: command.targetWindowNumber,
464
+ expectedFrame: command.targetWindowFrame
465
+ )
466
+ controller.release(autoHideMs: command.autoHideMs)
467
+ case "hide":
468
+ controller.hide()
469
+ case "stop":
470
+ controller.stop()
471
+ case "ping":
472
+ break
473
+ default:
474
+ throw CursorOverlayError(message: "unknown cursor overlay operation")
475
+ }
476
+ emitCursorResponse(["ok": true, "op": command.operation])
477
+ } catch let error as CursorOverlayError {
478
+ emitCursorResponse(["ok": false, "error": error.message])
479
+ } catch {
480
+ emitCursorResponse(["ok": false, "error": String(describing: error)])
481
+ }
482
+ }
483
+
484
+ private func emitCursorResponse(_ payload: [String: Any]) {
485
+ guard let data = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) else { return }
486
+ FileHandle.standardOutput.write(data)
487
+ FileHandle.standardOutput.write(Data("\n".utf8))
488
+ }