@mseep/clawdcursor 1.5.5

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 (354) hide show
  1. package/CHANGELOG.md +2264 -0
  2. package/LICENSE +21 -0
  3. package/README.md +385 -0
  4. package/SECURITY.md +44 -0
  5. package/SKILL.md +503 -0
  6. package/dist/core/agent-loop/agent.d.ts +42 -0
  7. package/dist/core/agent-loop/agent.js +1023 -0
  8. package/dist/core/agent-loop/agent.js.map +1 -0
  9. package/dist/core/agent-loop/batch-tool.d.ts +25 -0
  10. package/dist/core/agent-loop/batch-tool.js +218 -0
  11. package/dist/core/agent-loop/batch-tool.js.map +1 -0
  12. package/dist/core/agent-loop/coord-scale.d.ts +72 -0
  13. package/dist/core/agent-loop/coord-scale.js +89 -0
  14. package/dist/core/agent-loop/coord-scale.js.map +1 -0
  15. package/dist/core/agent-loop/focus-guard.d.ts +24 -0
  16. package/dist/core/agent-loop/focus-guard.js +29 -0
  17. package/dist/core/agent-loop/focus-guard.js.map +1 -0
  18. package/dist/core/agent-loop/project-mcp.d.ts +97 -0
  19. package/dist/core/agent-loop/project-mcp.js +253 -0
  20. package/dist/core/agent-loop/project-mcp.js.map +1 -0
  21. package/dist/core/agent-loop/prompt.d.ts +45 -0
  22. package/dist/core/agent-loop/prompt.js +426 -0
  23. package/dist/core/agent-loop/prompt.js.map +1 -0
  24. package/dist/core/agent-loop/tool-meta.d.ts +93 -0
  25. package/dist/core/agent-loop/tool-meta.js +651 -0
  26. package/dist/core/agent-loop/tool-meta.js.map +1 -0
  27. package/dist/core/agent-loop/tools.d.ts +38 -0
  28. package/dist/core/agent-loop/tools.js +2134 -0
  29. package/dist/core/agent-loop/tools.js.map +1 -0
  30. package/dist/core/agent-loop/types.d.ts +170 -0
  31. package/dist/core/agent-loop/types.js +12 -0
  32. package/dist/core/agent-loop/types.js.map +1 -0
  33. package/dist/core/agent.d.ts +51 -0
  34. package/dist/core/agent.js +245 -0
  35. package/dist/core/agent.js.map +1 -0
  36. package/dist/core/app-categories.d.ts +67 -0
  37. package/dist/core/app-categories.js +108 -0
  38. package/dist/core/app-categories.js.map +1 -0
  39. package/dist/core/banner.d.ts +70 -0
  40. package/dist/core/banner.js +245 -0
  41. package/dist/core/banner.js.map +1 -0
  42. package/dist/core/classify/capability.d.ts +45 -0
  43. package/dist/core/classify/capability.js +78 -0
  44. package/dist/core/classify/capability.js.map +1 -0
  45. package/dist/core/decompose/llm-decomposer.d.ts +35 -0
  46. package/dist/core/decompose/llm-decomposer.js +156 -0
  47. package/dist/core/decompose/llm-decomposer.js.map +1 -0
  48. package/dist/core/decompose/parser.d.ts +27 -0
  49. package/dist/core/decompose/parser.js +101 -0
  50. package/dist/core/decompose/parser.js.map +1 -0
  51. package/dist/core/observability/correlation.d.ts +19 -0
  52. package/dist/core/observability/correlation.js +36 -0
  53. package/dist/core/observability/correlation.js.map +1 -0
  54. package/dist/core/observability/cost-meter.d.ts +51 -0
  55. package/dist/core/observability/cost-meter.js +134 -0
  56. package/dist/core/observability/cost-meter.js.map +1 -0
  57. package/dist/core/observability/logger.d.ts +61 -0
  58. package/dist/core/observability/logger.js +550 -0
  59. package/dist/core/observability/logger.js.map +1 -0
  60. package/dist/core/router/aliases.d.ts +50 -0
  61. package/dist/core/router/aliases.js +104 -0
  62. package/dist/core/router/aliases.js.map +1 -0
  63. package/dist/core/router/normalize.d.ts +41 -0
  64. package/dist/core/router/normalize.js +80 -0
  65. package/dist/core/router/normalize.js.map +1 -0
  66. package/dist/core/safety.d.ts +126 -0
  67. package/dist/core/safety.js +568 -0
  68. package/dist/core/safety.js.map +1 -0
  69. package/dist/core/sense/a11y-resolver.d.ts +73 -0
  70. package/dist/core/sense/a11y-resolver.js +76 -0
  71. package/dist/core/sense/a11y-resolver.js.map +1 -0
  72. package/dist/core/sense/fingerprint.d.ts +41 -0
  73. package/dist/core/sense/fingerprint.js +123 -0
  74. package/dist/core/sense/fingerprint.js.map +1 -0
  75. package/dist/core/sense/rank.d.ts +70 -0
  76. package/dist/core/sense/rank.js +192 -0
  77. package/dist/core/sense/rank.js.map +1 -0
  78. package/dist/core/sense/reactive-check.d.ts +40 -0
  79. package/dist/core/sense/reactive-check.js +48 -0
  80. package/dist/core/sense/reactive-check.js.map +1 -0
  81. package/dist/core/sense/snapshot.d.ts +19 -0
  82. package/dist/core/sense/snapshot.js +100 -0
  83. package/dist/core/sense/snapshot.js.map +1 -0
  84. package/dist/core/sense/types.d.ts +66 -0
  85. package/dist/core/sense/types.js +9 -0
  86. package/dist/core/sense/types.js.map +1 -0
  87. package/dist/core/sense/ui-map-anchors.d.ts +7 -0
  88. package/dist/core/sense/ui-map-anchors.js +24 -0
  89. package/dist/core/sense/ui-map-anchors.js.map +1 -0
  90. package/dist/core/sense/ui-map-elements.d.ts +5 -0
  91. package/dist/core/sense/ui-map-elements.js +33 -0
  92. package/dist/core/sense/ui-map-elements.js.map +1 -0
  93. package/dist/core/sense/ui-map-find.d.ts +56 -0
  94. package/dist/core/sense/ui-map-find.js +153 -0
  95. package/dist/core/sense/ui-map-find.js.map +1 -0
  96. package/dist/core/sense/ui-map-fuse.d.ts +4 -0
  97. package/dist/core/sense/ui-map-fuse.js +44 -0
  98. package/dist/core/sense/ui-map-fuse.js.map +1 -0
  99. package/dist/core/sense/ui-map-geom.d.ts +3 -0
  100. package/dist/core/sense/ui-map-geom.js +16 -0
  101. package/dist/core/sense/ui-map-geom.js.map +1 -0
  102. package/dist/core/sense/ui-map-holder.d.ts +58 -0
  103. package/dist/core/sense/ui-map-holder.js +87 -0
  104. package/dist/core/sense/ui-map-holder.js.map +1 -0
  105. package/dist/core/sense/ui-map-normalize.d.ts +19 -0
  106. package/dist/core/sense/ui-map-normalize.js +65 -0
  107. package/dist/core/sense/ui-map-normalize.js.map +1 -0
  108. package/dist/core/sense/ui-map-render.d.ts +4 -0
  109. package/dist/core/sense/ui-map-render.js +34 -0
  110. package/dist/core/sense/ui-map-render.js.map +1 -0
  111. package/dist/core/sense/ui-map-resolve.d.ts +41 -0
  112. package/dist/core/sense/ui-map-resolve.js +59 -0
  113. package/dist/core/sense/ui-map-resolve.js.map +1 -0
  114. package/dist/core/sense/ui-map-types.d.ts +66 -0
  115. package/dist/core/sense/ui-map-types.js +11 -0
  116. package/dist/core/sense/ui-map-types.js.map +1 -0
  117. package/dist/core/sense/ui-map.d.ts +29 -0
  118. package/dist/core/sense/ui-map.js +113 -0
  119. package/dist/core/sense/ui-map.js.map +1 -0
  120. package/dist/core/verify/assertions.d.ts +132 -0
  121. package/dist/core/verify/assertions.js +284 -0
  122. package/dist/core/verify/assertions.js.map +1 -0
  123. package/dist/index.d.ts +21 -0
  124. package/dist/index.js +24 -0
  125. package/dist/index.js.map +1 -0
  126. package/dist/llm/browser-config.d.ts +36 -0
  127. package/dist/llm/browser-config.js +83 -0
  128. package/dist/llm/browser-config.js.map +1 -0
  129. package/dist/llm/client.d.ts +268 -0
  130. package/dist/llm/client.js +1094 -0
  131. package/dist/llm/client.js.map +1 -0
  132. package/dist/llm/config.d.ts +79 -0
  133. package/dist/llm/config.js +375 -0
  134. package/dist/llm/config.js.map +1 -0
  135. package/dist/llm/credentials.d.ts +35 -0
  136. package/dist/llm/credentials.js +491 -0
  137. package/dist/llm/credentials.js.map +1 -0
  138. package/dist/llm/external-creds.d.ts +42 -0
  139. package/dist/llm/external-creds.js +169 -0
  140. package/dist/llm/external-creds.js.map +1 -0
  141. package/dist/llm/providers.d.ts +123 -0
  142. package/dist/llm/providers.js +717 -0
  143. package/dist/llm/providers.js.map +1 -0
  144. package/dist/paths.d.ts +31 -0
  145. package/dist/paths.js +147 -0
  146. package/dist/paths.js.map +1 -0
  147. package/dist/platform/accessibility.d.ts +139 -0
  148. package/dist/platform/accessibility.js +670 -0
  149. package/dist/platform/accessibility.js.map +1 -0
  150. package/dist/platform/cdp-driver.d.ts +318 -0
  151. package/dist/platform/cdp-driver.js +1179 -0
  152. package/dist/platform/cdp-driver.js.map +1 -0
  153. package/dist/platform/index.d.ts +11 -0
  154. package/dist/platform/index.js +69 -0
  155. package/dist/platform/index.js.map +1 -0
  156. package/dist/platform/keys.d.ts +17 -0
  157. package/dist/platform/keys.js +129 -0
  158. package/dist/platform/keys.js.map +1 -0
  159. package/dist/platform/launch-poll.d.ts +101 -0
  160. package/dist/platform/launch-poll.js +177 -0
  161. package/dist/platform/launch-poll.js.map +1 -0
  162. package/dist/platform/linux.d.ts +173 -0
  163. package/dist/platform/linux.js +1253 -0
  164. package/dist/platform/linux.js.map +1 -0
  165. package/dist/platform/macos.d.ts +136 -0
  166. package/dist/platform/macos.js +976 -0
  167. package/dist/platform/macos.js.map +1 -0
  168. package/dist/platform/native-desktop.d.ts +145 -0
  169. package/dist/platform/native-desktop.js +936 -0
  170. package/dist/platform/native-desktop.js.map +1 -0
  171. package/dist/platform/native-helper.d.ts +130 -0
  172. package/dist/platform/native-helper.js +592 -0
  173. package/dist/platform/native-helper.js.map +1 -0
  174. package/dist/platform/ocr-engine.d.ts +78 -0
  175. package/dist/platform/ocr-engine.js +363 -0
  176. package/dist/platform/ocr-engine.js.map +1 -0
  177. package/dist/platform/ps-runner.d.ts +28 -0
  178. package/dist/platform/ps-runner.js +228 -0
  179. package/dist/platform/ps-runner.js.map +1 -0
  180. package/dist/platform/types.d.ts +397 -0
  181. package/dist/platform/types.js +15 -0
  182. package/dist/platform/types.js.map +1 -0
  183. package/dist/platform/uri-handler.d.ts +75 -0
  184. package/dist/platform/uri-handler.js +273 -0
  185. package/dist/platform/uri-handler.js.map +1 -0
  186. package/dist/platform/wayland-backend.d.ts +53 -0
  187. package/dist/platform/wayland-backend.js +348 -0
  188. package/dist/platform/wayland-backend.js.map +1 -0
  189. package/dist/platform/windows.d.ts +232 -0
  190. package/dist/platform/windows.js +1210 -0
  191. package/dist/platform/windows.js.map +1 -0
  192. package/dist/postbuild.d.ts +10 -0
  193. package/dist/postbuild.js +98 -0
  194. package/dist/postbuild.js.map +1 -0
  195. package/dist/schema/snapshot.d.ts +33 -0
  196. package/dist/schema/snapshot.js +90 -0
  197. package/dist/schema/snapshot.js.map +1 -0
  198. package/dist/shortcuts.d.ts +30 -0
  199. package/dist/shortcuts.js +261 -0
  200. package/dist/shortcuts.js.map +1 -0
  201. package/dist/surface/cli.d.ts +7 -0
  202. package/dist/surface/cli.js +1556 -0
  203. package/dist/surface/cli.js.map +1 -0
  204. package/dist/surface/dashboard.d.ts +8 -0
  205. package/dist/surface/dashboard.js +1193 -0
  206. package/dist/surface/dashboard.js.map +1 -0
  207. package/dist/surface/doctor.d.ts +29 -0
  208. package/dist/surface/doctor.js +1514 -0
  209. package/dist/surface/doctor.js.map +1 -0
  210. package/dist/surface/format.d.ts +10 -0
  211. package/dist/surface/format.js +37 -0
  212. package/dist/surface/format.js.map +1 -0
  213. package/dist/surface/http-utility.d.ts +65 -0
  214. package/dist/surface/http-utility.js +336 -0
  215. package/dist/surface/http-utility.js.map +1 -0
  216. package/dist/surface/mcp-server.d.ts +91 -0
  217. package/dist/surface/mcp-server.js +280 -0
  218. package/dist/surface/mcp-server.js.map +1 -0
  219. package/dist/surface/onboarding.d.ts +15 -0
  220. package/dist/surface/onboarding.js +184 -0
  221. package/dist/surface/onboarding.js.map +1 -0
  222. package/dist/surface/pidfile.d.ts +79 -0
  223. package/dist/surface/pidfile.js +263 -0
  224. package/dist/surface/pidfile.js.map +1 -0
  225. package/dist/surface/readiness.d.ts +45 -0
  226. package/dist/surface/readiness.js +230 -0
  227. package/dist/surface/readiness.js.map +1 -0
  228. package/dist/surface/report.d.ts +68 -0
  229. package/dist/surface/report.js +341 -0
  230. package/dist/surface/report.js.map +1 -0
  231. package/dist/surface/skill-register.d.ts +14 -0
  232. package/dist/surface/skill-register.js +150 -0
  233. package/dist/surface/skill-register.js.map +1 -0
  234. package/dist/surface/version.d.ts +6 -0
  235. package/dist/surface/version.js +27 -0
  236. package/dist/surface/version.js.map +1 -0
  237. package/dist/tools/a11y.d.ts +8 -0
  238. package/dist/tools/a11y.js +545 -0
  239. package/dist/tools/a11y.js.map +1 -0
  240. package/dist/tools/a11y_depth.d.ts +19 -0
  241. package/dist/tools/a11y_depth.js +455 -0
  242. package/dist/tools/a11y_depth.js.map +1 -0
  243. package/dist/tools/agent.d.ts +15 -0
  244. package/dist/tools/agent.js +248 -0
  245. package/dist/tools/agent.js.map +1 -0
  246. package/dist/tools/batch.d.ts +46 -0
  247. package/dist/tools/batch.js +230 -0
  248. package/dist/tools/batch.js.map +1 -0
  249. package/dist/tools/cdp.d.ts +8 -0
  250. package/dist/tools/cdp.js +233 -0
  251. package/dist/tools/cdp.js.map +1 -0
  252. package/dist/tools/compact.d.ts +63 -0
  253. package/dist/tools/compact.js +418 -0
  254. package/dist/tools/compact.js.map +1 -0
  255. package/dist/tools/cost-class.d.ts +38 -0
  256. package/dist/tools/cost-class.js +117 -0
  257. package/dist/tools/cost-class.js.map +1 -0
  258. package/dist/tools/desktop.d.ts +9 -0
  259. package/dist/tools/desktop.js +346 -0
  260. package/dist/tools/desktop.js.map +1 -0
  261. package/dist/tools/electron_bridge.d.ts +41 -0
  262. package/dist/tools/electron_bridge.js +261 -0
  263. package/dist/tools/electron_bridge.js.map +1 -0
  264. package/dist/tools/extras.d.ts +22 -0
  265. package/dist/tools/extras.js +942 -0
  266. package/dist/tools/extras.js.map +1 -0
  267. package/dist/tools/favorites.d.ts +13 -0
  268. package/dist/tools/favorites.js +137 -0
  269. package/dist/tools/favorites.js.map +1 -0
  270. package/dist/tools/introspection.d.ts +13 -0
  271. package/dist/tools/introspection.js +55 -0
  272. package/dist/tools/introspection.js.map +1 -0
  273. package/dist/tools/ocr.d.ts +8 -0
  274. package/dist/tools/ocr.js +66 -0
  275. package/dist/tools/ocr.js.map +1 -0
  276. package/dist/tools/orchestration.d.ts +7 -0
  277. package/dist/tools/orchestration.js +377 -0
  278. package/dist/tools/orchestration.js.map +1 -0
  279. package/dist/tools/playbooks/extract-compose.d.ts +22 -0
  280. package/dist/tools/playbooks/extract-compose.js +85 -0
  281. package/dist/tools/playbooks/extract-compose.js.map +1 -0
  282. package/dist/tools/playbooks/find-replace.d.ts +11 -0
  283. package/dist/tools/playbooks/find-replace.js +56 -0
  284. package/dist/tools/playbooks/find-replace.js.map +1 -0
  285. package/dist/tools/playbooks/index.d.ts +63 -0
  286. package/dist/tools/playbooks/index.js +70 -0
  287. package/dist/tools/playbooks/index.js.map +1 -0
  288. package/dist/tools/playbooks/keys-blocklist.d.ts +24 -0
  289. package/dist/tools/playbooks/keys-blocklist.js +89 -0
  290. package/dist/tools/playbooks/keys-blocklist.js.map +1 -0
  291. package/dist/tools/registry.d.ts +40 -0
  292. package/dist/tools/registry.js +560 -0
  293. package/dist/tools/registry.js.map +1 -0
  294. package/dist/tools/safety-gate.d.ts +16 -0
  295. package/dist/tools/safety-gate.js +70 -0
  296. package/dist/tools/safety-gate.js.map +1 -0
  297. package/dist/tools/scheduler.d.ts +76 -0
  298. package/dist/tools/scheduler.js +413 -0
  299. package/dist/tools/scheduler.js.map +1 -0
  300. package/dist/tools/shortcuts.d.ts +13 -0
  301. package/dist/tools/shortcuts.js +205 -0
  302. package/dist/tools/shortcuts.js.map +1 -0
  303. package/dist/tools/smart.d.ts +15 -0
  304. package/dist/tools/smart.js +785 -0
  305. package/dist/tools/smart.js.map +1 -0
  306. package/dist/tools/types.d.ts +174 -0
  307. package/dist/tools/types.js +67 -0
  308. package/dist/tools/types.js.map +1 -0
  309. package/dist/tools/window-text.d.ts +15 -0
  310. package/dist/tools/window-text.js +39 -0
  311. package/dist/tools/window-text.js.map +1 -0
  312. package/dist/types.d.ts +122 -0
  313. package/dist/types.js +41 -0
  314. package/dist/types.js.map +1 -0
  315. package/native/Package.swift +38 -0
  316. package/native/README.md +113 -0
  317. package/native/Sources/ClawdCursorHelper/main.swift +602 -0
  318. package/native/Sources/ClawdCursorHost/main.swift +182 -0
  319. package/native/Sources/PermissionCheck/main.swift +53 -0
  320. package/native/Sources/ScreenshotHelper/main.swift +219 -0
  321. package/native/build.sh +139 -0
  322. package/native/entitlements.plist +12 -0
  323. package/package.json +115 -0
  324. package/scripts/banner.ps1 +112 -0
  325. package/scripts/coord-accuracy.ps1 +140 -0
  326. package/scripts/coord-uwp.ps1 +80 -0
  327. package/scripts/edge-glow.ps1 +180 -0
  328. package/scripts/find-element.ps1 +198 -0
  329. package/scripts/get-foreground-window.ps1 +71 -0
  330. package/scripts/get-screen-context.ps1 +183 -0
  331. package/scripts/get-windows.ps1 +66 -0
  332. package/scripts/install-panic-hotkey.ps1 +46 -0
  333. package/scripts/interact-element.ps1 +431 -0
  334. package/scripts/invoke-element.ps1 +314 -0
  335. package/scripts/linux/atspi-bridge.py +356 -0
  336. package/scripts/linux/ocr-recognize.py +154 -0
  337. package/scripts/mac/_window-picker.jxa +163 -0
  338. package/scripts/mac/find-element.jxa +0 -0
  339. package/scripts/mac/find-element.sh +161 -0
  340. package/scripts/mac/focus-window.jxa +284 -0
  341. package/scripts/mac/get-focused-element.jxa +102 -0
  342. package/scripts/mac/get-foreground-window.jxa +173 -0
  343. package/scripts/mac/get-screen-context.jxa +197 -0
  344. package/scripts/mac/get-ui-tree.sh +141 -0
  345. package/scripts/mac/get-windows.jxa +117 -0
  346. package/scripts/mac/interact-element.sh +235 -0
  347. package/scripts/mac/invoke-element.jxa +408 -0
  348. package/scripts/mac/ocr-recognize.swift +124 -0
  349. package/scripts/ocr-recognize.ps1 +102 -0
  350. package/scripts/postinstall-native.js +48 -0
  351. package/scripts/ps-bridge.ps1 +830 -0
  352. package/scripts/smoke-mcp.ps1 +119 -0
  353. package/scripts/sync-version.ts +178 -0
  354. package/scripts/verify-install.js +81 -0
@@ -0,0 +1,602 @@
1
+ /// ClawdCursor Native Helper - macOS Accessibility & Input Control
2
+ /// Communicates with Node.js via JSON-RPC over stdio
3
+ ///
4
+ /// Commands:
5
+ /// - checkPermissions: Returns permission status
6
+ /// - traverseAccessibilityTree: Returns UI element tree for a PID
7
+ /// - click: Click at coordinates or element
8
+ /// - type: Type text
9
+ /// - pressKey: Press key combination
10
+ /// - openApp: Open application by name or bundle ID
11
+ /// - getWindowList: List visible windows
12
+
13
+ import Foundation
14
+ import ApplicationServices
15
+ import CoreGraphics
16
+ import AppKit
17
+ import ImageIO
18
+ import UniformTypeIdentifiers
19
+
20
+ // MARK: - JSON-RPC Types
21
+
22
+ struct JsonRpcRequest: Codable {
23
+ let id: Int
24
+ let method: String
25
+ let params: [String: AnyCodable]?
26
+ }
27
+
28
+ struct JsonRpcResponse: Codable {
29
+ let id: Int
30
+ let result: AnyCodable?
31
+ let error: JsonRpcError?
32
+ }
33
+
34
+ struct JsonRpcError: Codable {
35
+ let code: Int
36
+ let message: String
37
+ }
38
+
39
+ // AnyCodable wrapper for dynamic JSON
40
+ struct AnyCodable: Codable {
41
+ let value: Any
42
+
43
+ init(_ value: Any) {
44
+ self.value = value
45
+ }
46
+
47
+ init(from decoder: Decoder) throws {
48
+ let container = try decoder.singleValueContainer()
49
+ if container.decodeNil() {
50
+ value = NSNull()
51
+ } else if let bool = try? container.decode(Bool.self) {
52
+ value = bool
53
+ } else if let int = try? container.decode(Int.self) {
54
+ value = int
55
+ } else if let double = try? container.decode(Double.self) {
56
+ value = double
57
+ } else if let string = try? container.decode(String.self) {
58
+ value = string
59
+ } else if let array = try? container.decode([AnyCodable].self) {
60
+ value = array.map { $0.value }
61
+ } else if let dict = try? container.decode([String: AnyCodable].self) {
62
+ value = dict.mapValues { $0.value }
63
+ } else {
64
+ throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode value")
65
+ }
66
+ }
67
+
68
+ func encode(to encoder: Encoder) throws {
69
+ var container = encoder.singleValueContainer()
70
+ switch value {
71
+ case is NSNull:
72
+ try container.encodeNil()
73
+ case let bool as Bool:
74
+ try container.encode(bool)
75
+ case let int as Int:
76
+ try container.encode(int)
77
+ case let double as Double:
78
+ try container.encode(double)
79
+ case let string as String:
80
+ try container.encode(string)
81
+ case let array as [Any]:
82
+ try container.encode(array.map { AnyCodable($0) })
83
+ case let dict as [String: Any]:
84
+ try container.encode(dict.mapValues { AnyCodable($0) })
85
+ default:
86
+ try container.encode(String(describing: value))
87
+ }
88
+ }
89
+ }
90
+
91
+ // MARK: - Accessibility Element
92
+
93
+ struct UIElement: Codable {
94
+ let role: String?
95
+ let title: String?
96
+ let value: String?
97
+ let description: String?
98
+ let position: [String: CGFloat]?
99
+ let size: [String: CGFloat]?
100
+ let enabled: Bool
101
+ let focused: Bool
102
+ let children: [UIElement]?
103
+ }
104
+
105
+ // MARK: - Main Handler
106
+
107
+ class ClawdCursorHelper {
108
+ static let shared = ClawdCursorHelper()
109
+
110
+ /// Map a character to its macOS virtual keycode (US ANSI layout).
111
+ /// Covers a-z, 0-9, and common symbols — enough for all keyboard shortcuts.
112
+ /// Returns nil if the character has no known keycode mapping.
113
+ static func keycodeForCharacter(_ scalar: Unicode.Scalar) -> CGKeyCode? {
114
+ let c = Character(scalar).lowercased()
115
+ let map: [String: CGKeyCode] = [
116
+ "a": 0x00, "s": 0x01, "d": 0x02, "f": 0x03, "h": 0x04, "g": 0x05,
117
+ "z": 0x06, "x": 0x07, "c": 0x08, "v": 0x09, "b": 0x0B, "q": 0x0C,
118
+ "w": 0x0D, "e": 0x0E, "r": 0x0F, "y": 0x10, "t": 0x11, "1": 0x12,
119
+ "2": 0x13, "3": 0x14, "4": 0x15, "6": 0x16, "5": 0x17, "=": 0x18,
120
+ "9": 0x19, "7": 0x1A, "-": 0x1B, "8": 0x1C, "0": 0x1D, "]": 0x1E,
121
+ "o": 0x1F, "u": 0x20, "[": 0x21, "i": 0x22, "p": 0x23, "l": 0x25,
122
+ "j": 0x26, "'": 0x27, "k": 0x28, ";": 0x29, "\\": 0x2A, ",": 0x2B,
123
+ "/": 0x2C, "n": 0x2D, "m": 0x2E, ".": 0x2F, "`": 0x32,
124
+ "+": 0x18, "*": 0x43, // + maps to = key, * maps to numpad multiply
125
+ ]
126
+ return map[c]
127
+ }
128
+
129
+ private let encoder: JSONEncoder = {
130
+ let e = JSONEncoder()
131
+ e.outputFormatting = [.sortedKeys]
132
+ return e
133
+ }()
134
+
135
+ private let decoder = JSONDecoder()
136
+
137
+ func run() {
138
+ // Check accessibility permission at startup (pattern from mediar-ai/MacosUseSDK)
139
+ let axOptions = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: kCFBooleanFalse] as CFDictionary
140
+ if !AXIsProcessTrustedWithOptions(axOptions) {
141
+ fputs("{\"error\": \"accessibility_denied\", \"message\": \"Grant Accessibility permission in System Settings > Privacy & Security > Accessibility\"}\n", stderr)
142
+ // Continue anyway - some commands might not need it
143
+ }
144
+
145
+ // Read JSON-RPC requests from stdin
146
+ while let line = readLine() {
147
+ guard !line.isEmpty else { continue }
148
+ handleRequest(line)
149
+ }
150
+ }
151
+
152
+ func handleRequest(_ line: String) {
153
+ guard let data = line.data(using: .utf8),
154
+ let request = try? decoder.decode(JsonRpcRequest.self, from: data) else {
155
+ fputs("{\"error\": \"parse_error\", \"message\": \"Invalid JSON-RPC request\"}\n", stderr)
156
+ return
157
+ }
158
+
159
+ let response: JsonRpcResponse
160
+
161
+ switch request.method {
162
+ case "checkPermissions":
163
+ response = checkPermissions(id: request.id)
164
+ case "traverseAccessibilityTree":
165
+ response = traverseAccessibilityTree(id: request.id, params: request.params)
166
+ case "click":
167
+ response = click(id: request.id, params: request.params)
168
+ case "moveMouse":
169
+ response = moveMouse(id: request.id, params: request.params)
170
+ case "dragMouse":
171
+ response = dragMouse(id: request.id, params: request.params)
172
+ case "type":
173
+ response = typeText(id: request.id, params: request.params)
174
+ case "pressKey":
175
+ response = pressKey(id: request.id, params: request.params)
176
+ case "captureScreen":
177
+ response = captureScreen(id: request.id)
178
+ case "openApp":
179
+ response = openApp(id: request.id, params: request.params)
180
+ case "getWindowList":
181
+ response = getWindowList(id: request.id)
182
+ default:
183
+ response = JsonRpcResponse(id: request.id, result: nil, error: JsonRpcError(code: -32601, message: "Method not found: \(request.method)"))
184
+ }
185
+
186
+ if let responseData = try? encoder.encode(response),
187
+ let responseString = String(data: responseData, encoding: .utf8) {
188
+ print(responseString)
189
+ fflush(stdout)
190
+ }
191
+ }
192
+
193
+ // MARK: - Commands
194
+
195
+ func checkPermissions(id: Int) -> JsonRpcResponse {
196
+ // Pattern from mediar-ai/MacosUseSDK
197
+ let axOptions = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: kCFBooleanFalse] as CFDictionary
198
+ let axGranted = AXIsProcessTrustedWithOptions(axOptions)
199
+ let screenGranted = CGPreflightScreenCaptureAccess()
200
+ let processPath = ProcessInfo.processInfo.arguments.first ?? "unknown"
201
+ let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
202
+
203
+ return JsonRpcResponse(id: id, result: AnyCodable([
204
+ "accessibility": axGranted,
205
+ "screenRecording": screenGranted,
206
+ "processPath": processPath,
207
+ "bundleId": bundleId
208
+ ]), error: nil)
209
+ }
210
+
211
+ func traverseAccessibilityTree(id: Int, params: [String: AnyCodable]?) -> JsonRpcResponse {
212
+ guard let pid = params?["pid"]?.value as? Int else {
213
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Missing 'pid' parameter"))
214
+ }
215
+
216
+ // Caps from mediar-ai/MacosUseSDK - prevents hanging on complex apps
217
+ let maxDepth = (params?["maxDepth"]?.value as? Int) ?? 100
218
+ let maxElements = (params?["maxElements"]?.value as? Int) ?? 2000
219
+ let maxSeconds: Double = 5.0
220
+ let startTime = Date()
221
+
222
+ let app = AXUIElementCreateApplication(pid_t(pid))
223
+ AXUIElementSetMessagingTimeout(app, 5.0)
224
+ var elementCount = 0
225
+ var truncated = false
226
+
227
+ func traverse(_ element: AXUIElement, depth: Int) -> UIElement? {
228
+ // Check all caps
229
+ if depth >= maxDepth || elementCount >= maxElements {
230
+ truncated = true
231
+ return nil
232
+ }
233
+ if Date().timeIntervalSince(startTime) > maxSeconds {
234
+ truncated = true
235
+ return nil
236
+ }
237
+ elementCount += 1
238
+ AXUIElementSetMessagingTimeout(element, 2.0)
239
+
240
+ var role: CFTypeRef?
241
+ var title: CFTypeRef?
242
+ var value: CFTypeRef?
243
+ var desc: CFTypeRef?
244
+ var position: CFTypeRef?
245
+ var size: CFTypeRef?
246
+ var enabled: CFTypeRef?
247
+ var focused: CFTypeRef?
248
+ var children: CFTypeRef?
249
+
250
+ AXUIElementCopyAttributeValue(element, kAXRoleAttribute as CFString, &role)
251
+ AXUIElementCopyAttributeValue(element, kAXTitleAttribute as CFString, &title)
252
+ AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, &value)
253
+ AXUIElementCopyAttributeValue(element, kAXDescriptionAttribute as CFString, &desc)
254
+ AXUIElementCopyAttributeValue(element, kAXPositionAttribute as CFString, &position)
255
+ AXUIElementCopyAttributeValue(element, kAXSizeAttribute as CFString, &size)
256
+ AXUIElementCopyAttributeValue(element, kAXEnabledAttribute as CFString, &enabled)
257
+ AXUIElementCopyAttributeValue(element, kAXFocusedAttribute as CFString, &focused)
258
+ AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &children)
259
+
260
+ var posDict: [String: CGFloat]? = nil
261
+ if let pos = position, CFGetTypeID(pos) == AXValueGetTypeID() {
262
+ var point = CGPoint.zero
263
+ if AXValueGetValue(pos as! AXValue, .cgPoint, &point) {
264
+ posDict = ["x": point.x, "y": point.y]
265
+ }
266
+ }
267
+
268
+ var sizeDict: [String: CGFloat]? = nil
269
+ if let sz = size, CFGetTypeID(sz) == AXValueGetTypeID() {
270
+ var s = CGSize.zero
271
+ if AXValueGetValue(sz as! AXValue, .cgSize, &s) {
272
+ sizeDict = ["width": s.width, "height": s.height]
273
+ }
274
+ }
275
+
276
+ var childElements: [UIElement]? = nil
277
+ if let childArray = children as? [AXUIElement], !childArray.isEmpty {
278
+ childElements = childArray.compactMap { traverse($0, depth: depth + 1) }
279
+ }
280
+
281
+ return UIElement(
282
+ role: role as? String,
283
+ title: title as? String,
284
+ value: value as? String,
285
+ description: desc as? String,
286
+ position: posDict,
287
+ size: sizeDict,
288
+ enabled: (enabled as? Bool) ?? true,
289
+ focused: (focused as? Bool) ?? false,
290
+ children: childElements
291
+ )
292
+ }
293
+
294
+ if let rootElement = traverse(app, depth: 0) {
295
+ return JsonRpcResponse(id: id, result: AnyCodable([
296
+ "pid": pid,
297
+ "elementCount": elementCount,
298
+ "truncated": truncated,
299
+ "tree": rootElement
300
+ ]), error: nil)
301
+ } else {
302
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32000, message: "Failed to traverse accessibility tree for PID \(pid)"))
303
+ }
304
+ }
305
+
306
+ /// Safely extract a Double from AnyCodable that may contain Int or Double
307
+ private func asDouble(_ val: Any?) -> Double? {
308
+ if let d = val as? Double { return d }
309
+ if let i = val as? Int { return Double(i) }
310
+ if let s = val as? String { return Double(s) }
311
+ return nil
312
+ }
313
+
314
+ func click(id: Int, params: [String: AnyCodable]?) -> JsonRpcResponse {
315
+ guard let x = asDouble(params?["x"]?.value),
316
+ let y = asDouble(params?["y"]?.value) else {
317
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Missing 'x' or 'y' parameter"))
318
+ }
319
+
320
+ let point = CGPoint(x: x, y: y)
321
+ let button = (params?["button"]?.value as? String) == "right" ? CGMouseButton.right : CGMouseButton.left
322
+ let clickCount = (params?["clickCount"]?.value as? Int) ?? 1
323
+
324
+ let downType: CGEventType = button == .right ? .rightMouseDown : .leftMouseDown
325
+ let upType: CGEventType = button == .right ? .rightMouseUp : .leftMouseUp
326
+
327
+ for _ in 0..<clickCount {
328
+ if let mouseDown = CGEvent(mouseEventSource: nil, mouseType: downType, mouseCursorPosition: point, mouseButton: button) {
329
+ mouseDown.post(tap: .cghidEventTap)
330
+ }
331
+ usleep(10000) // 10ms
332
+ if let mouseUp = CGEvent(mouseEventSource: nil, mouseType: upType, mouseCursorPosition: point, mouseButton: button) {
333
+ mouseUp.post(tap: .cghidEventTap)
334
+ }
335
+ usleep(50000) // 50ms between clicks
336
+ }
337
+
338
+ return JsonRpcResponse(id: id, result: AnyCodable(["success": true, "x": x, "y": y]), error: nil)
339
+ }
340
+
341
+ func moveMouse(id: Int, params: [String: AnyCodable]?) -> JsonRpcResponse {
342
+ guard let x = asDouble(params?["x"]?.value),
343
+ let y = asDouble(params?["y"]?.value) else {
344
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Missing 'x' or 'y' parameter"))
345
+ }
346
+ let point = CGPoint(x: x, y: y)
347
+ if let event = CGEvent(mouseEventSource: nil, mouseType: .mouseMoved, mouseCursorPosition: point, mouseButton: .left) {
348
+ event.post(tap: .cghidEventTap)
349
+ }
350
+ return JsonRpcResponse(id: id, result: AnyCodable(["success": true, "x": x, "y": y]), error: nil)
351
+ }
352
+
353
+ func dragMouse(id: Int, params: [String: AnyCodable]?) -> JsonRpcResponse {
354
+ guard let startX = asDouble(params?["startX"]?.value),
355
+ let startY = asDouble(params?["startY"]?.value),
356
+ let endX = asDouble(params?["endX"]?.value),
357
+ let endY = asDouble(params?["endY"]?.value) else {
358
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Missing drag coordinates"))
359
+ }
360
+
361
+ let startPoint = CGPoint(x: startX, y: startY)
362
+ let endPoint = CGPoint(x: endX, y: endY)
363
+ let button: CGMouseButton = .left
364
+
365
+ if let mouseDown = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown, mouseCursorPosition: startPoint, mouseButton: button) {
366
+ mouseDown.post(tap: .cghidEventTap)
367
+ }
368
+ usleep(30000)
369
+
370
+ let steps = max(5, Int(hypot(endX - startX, endY - startY) / 20.0))
371
+ for i in 1...steps {
372
+ let t = Double(i) / Double(steps)
373
+ let ix = startX + (endX - startX) * t
374
+ let iy = startY + (endY - startY) * t
375
+ let point = CGPoint(x: ix, y: iy)
376
+ if let drag = CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged, mouseCursorPosition: point, mouseButton: button) {
377
+ drag.post(tap: .cghidEventTap)
378
+ }
379
+ usleep(12000)
380
+ }
381
+
382
+ if let mouseUp = CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp, mouseCursorPosition: endPoint, mouseButton: button) {
383
+ mouseUp.post(tap: .cghidEventTap)
384
+ }
385
+
386
+ return JsonRpcResponse(id: id, result: AnyCodable(["success": true]), error: nil)
387
+ }
388
+
389
+ func typeText(id: Int, params: [String: AnyCodable]?) -> JsonRpcResponse {
390
+ guard let text = params?["text"]?.value as? String else {
391
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Missing 'text' parameter"))
392
+ }
393
+
394
+ let delayMs = (params?["delayMs"]?.value as? Int) ?? 10
395
+
396
+ for char in text {
397
+ let source = CGEventSource(stateID: .hidSystemState)
398
+ if let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: true) {
399
+ var buffer = [UniChar](String(char).utf16)
400
+ keyDown.keyboardSetUnicodeString(stringLength: buffer.count, unicodeString: &buffer)
401
+ keyDown.post(tap: .cghidEventTap)
402
+ }
403
+ if let keyUp = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: false) {
404
+ keyUp.post(tap: .cghidEventTap)
405
+ }
406
+ usleep(UInt32(delayMs * 1000))
407
+ }
408
+
409
+ return JsonRpcResponse(id: id, result: AnyCodable(["success": true, "length": text.count]), error: nil)
410
+ }
411
+
412
+ func pressKey(id: Int, params: [String: AnyCodable]?) -> JsonRpcResponse {
413
+ guard let key = params?["key"]?.value as? String else {
414
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Missing 'key' parameter"))
415
+ }
416
+
417
+ let modifiers = params?["modifiers"]?.value as? [String] ?? []
418
+
419
+ // Map key names to virtual keycodes
420
+ let keyCode: CGKeyCode
421
+ switch key.lowercased() {
422
+ case "return", "enter": keyCode = 0x24
423
+ case "tab": keyCode = 0x30
424
+ case "space": keyCode = 0x31
425
+ case "delete", "backspace": keyCode = 0x33
426
+ case "escape", "esc": keyCode = 0x35
427
+ case "left": keyCode = 0x7B
428
+ case "right": keyCode = 0x7C
429
+ case "down": keyCode = 0x7D
430
+ case "up": keyCode = 0x7E
431
+ case "f1": keyCode = 0x7A
432
+ case "f2": keyCode = 0x78
433
+ case "f3": keyCode = 0x63
434
+ case "f4": keyCode = 0x76
435
+ case "f5": keyCode = 0x60
436
+ case "f6": keyCode = 0x61
437
+ case "f7": keyCode = 0x62
438
+ case "f8": keyCode = 0x64
439
+ case "f9": keyCode = 0x65
440
+ case "f10": keyCode = 0x6D
441
+ case "f11": keyCode = 0x67
442
+ case "f12": keyCode = 0x6F
443
+ default:
444
+ // For single characters: look up keycode from character, or use unicode event.
445
+ // CRITICAL: modifiers must NOT be discarded — cmd+v, cmd+n, shift+cmd+d all depend on this.
446
+ if key.count == 1, let scalar = key.unicodeScalars.first {
447
+ // Try common ASCII keycode mapping first (covers a-z, 0-9, symbols)
448
+ guard let kc = Self.keycodeForCharacter(scalar) else {
449
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Unsupported key character: \(key) (not in ANSI keycode map)"))
450
+ }
451
+ keyCode = kc
452
+ } else {
453
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32602, message: "Unknown key: \(key)"))
454
+ }
455
+ }
456
+
457
+ var flags: CGEventFlags = []
458
+ for mod in modifiers {
459
+ switch mod.lowercased() {
460
+ case "cmd", "command": flags.insert(.maskCommand)
461
+ case "shift": flags.insert(.maskShift)
462
+ case "alt", "option": flags.insert(.maskAlternate)
463
+ case "ctrl", "control": flags.insert(.maskControl)
464
+ default: break
465
+ }
466
+ }
467
+
468
+ let source = CGEventSource(stateID: .hidSystemState)
469
+ if let keyDown = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true) {
470
+ keyDown.flags = flags
471
+ keyDown.post(tap: .cghidEventTap)
472
+ }
473
+ usleep(10000)
474
+ if let keyUp = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false) {
475
+ keyUp.flags = flags
476
+ keyUp.post(tap: .cghidEventTap)
477
+ }
478
+
479
+ return JsonRpcResponse(id: id, result: AnyCodable(["success": true, "key": key, "modifiers": modifiers]), error: nil)
480
+ }
481
+
482
+ func captureScreen(id: Int) -> JsonRpcResponse {
483
+ guard CGPreflightScreenCaptureAccess() else {
484
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32001, message: "screen_recording_denied"))
485
+ }
486
+
487
+ let tempPath = (NSTemporaryDirectory() as NSString).appendingPathComponent("clawdcursor-capture-\(UUID().uuidString).png")
488
+ let proc = Process()
489
+ proc.executableURL = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/screenshot-helper")
490
+ proc.arguments = ["--fullscreen", tempPath]
491
+
492
+ let stderr = Pipe()
493
+ proc.standardError = stderr
494
+
495
+ do {
496
+ try proc.run()
497
+ proc.waitUntilExit()
498
+ } catch {
499
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32000, message: "Failed to launch screenshot-helper: \(error.localizedDescription)"))
500
+ }
501
+
502
+ guard proc.terminationStatus == 0 else {
503
+ let errData = stderr.fileHandleForReading.readDataToEndOfFile()
504
+ let errText = String(data: errData, encoding: .utf8) ?? "unknown screenshot-helper error"
505
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32000, message: "screenshot-helper failed: \(errText.trimmingCharacters(in: .whitespacesAndNewlines))"))
506
+ }
507
+
508
+ let url = URL(fileURLWithPath: tempPath)
509
+ defer { try? FileManager.default.removeItem(at: url) }
510
+
511
+ guard let data = try? Data(contentsOf: url),
512
+ let image = CGImageSourceCreateWithURL(url as CFURL, nil).flatMap({ CGImageSourceCreateImageAtIndex($0, 0, nil) }) else {
513
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32000, message: "Failed to read screenshot-helper output"))
514
+ }
515
+
516
+ let base64 = data.base64EncodedString()
517
+ return JsonRpcResponse(id: id, result: AnyCodable([
518
+ "success": true,
519
+ "width": image.width,
520
+ "height": image.height,
521
+ "format": "png",
522
+ "imageBase64": base64
523
+ ]), error: nil)
524
+ }
525
+
526
+ func openApp(id: Int, params: [String: AnyCodable]?) -> JsonRpcResponse {
527
+ let appName = params?["name"]?.value as? String
528
+ let bundleId = params?["bundleId"]?.value as? String
529
+
530
+ var url: URL?
531
+
532
+ if let bundleId = bundleId {
533
+ url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleId)
534
+ } else if let appName = appName {
535
+ url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: appName)
536
+ if url == nil {
537
+ // Try finding by name
538
+ let path = "/Applications/\(appName).app"
539
+ if FileManager.default.fileExists(atPath: path) {
540
+ url = URL(fileURLWithPath: path)
541
+ }
542
+ }
543
+ }
544
+
545
+ guard let appUrl = url else {
546
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32000, message: "App not found"))
547
+ }
548
+
549
+ let config = NSWorkspace.OpenConfiguration()
550
+ config.activates = true
551
+
552
+ let semaphore = DispatchSemaphore(value: 0)
553
+ var resultPid: pid_t = 0
554
+ var resultError: Error?
555
+
556
+ NSWorkspace.shared.openApplication(at: appUrl, configuration: config) { app, error in
557
+ resultPid = app?.processIdentifier ?? 0
558
+ resultError = error
559
+ semaphore.signal()
560
+ }
561
+
562
+ semaphore.wait()
563
+
564
+ if let error = resultError {
565
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32000, message: error.localizedDescription))
566
+ }
567
+
568
+ return JsonRpcResponse(id: id, result: AnyCodable(["success": true, "pid": Int(resultPid)]), error: nil)
569
+ }
570
+
571
+ func getWindowList(id: Int) -> JsonRpcResponse {
572
+ let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
573
+ guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {
574
+ return JsonRpcResponse(id: id, result: nil, error: JsonRpcError(code: -32000, message: "Failed to get window list"))
575
+ }
576
+
577
+ let windows: [[String: Any]] = windowList.compactMap { window in
578
+ guard let ownerPid = window[kCGWindowOwnerPID as String] as? Int,
579
+ let windowId = window[kCGWindowNumber as String] as? Int,
580
+ let layer = window[kCGWindowLayer as String] as? Int,
581
+ layer == 0 else { return nil } // Normal windows only
582
+
583
+ let ownerName = window[kCGWindowOwnerName as String] as? String ?? ""
584
+ let windowName = window[kCGWindowName as String] as? String ?? ""
585
+ let bounds = window[kCGWindowBounds as String] as? [String: CGFloat] ?? [:]
586
+
587
+ return [
588
+ "windowId": windowId,
589
+ "ownerPid": ownerPid,
590
+ "ownerName": ownerName,
591
+ "windowName": windowName,
592
+ "bounds": bounds
593
+ ]
594
+ }
595
+
596
+ return JsonRpcResponse(id: id, result: AnyCodable(["windows": windows]), error: nil)
597
+ }
598
+ }
599
+
600
+ // MARK: - Main
601
+
602
+ ClawdCursorHelper.shared.run()