@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,976 @@
1
+ "use strict";
2
+ /**
3
+ * macOS PlatformAdapter — all macOS-specific code lives here.
4
+ *
5
+ * Strategy:
6
+ * - Keystrokes: osascript + System Events (CGEvent from helper is blocked by TCC)
7
+ * - Mouse: nut-js (nut-js mouse events ARE delivered)
8
+ * - Screenshot: screenshot-helper Swift binary (avoids ReplayKit CPU spin)
9
+ * - Accessibility: osascript JXA for tree queries + permission-check binary for TCC
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.MacOSAdapter = void 0;
49
+ const path = __importStar(require("path"));
50
+ const os = __importStar(require("os"));
51
+ const fs = __importStar(require("fs"));
52
+ const child_process_1 = require("child_process");
53
+ const util_1 = require("util");
54
+ const sharp_1 = __importDefault(require("sharp"));
55
+ const nut_js_1 = require("@nut-tree-fork/nut-js");
56
+ const launch_poll_1 = require("./launch-poll");
57
+ const paths_1 = require("../paths");
58
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
59
+ const SCRIPTS_DIR = path.join((0, paths_1.getPackageRoot)(), 'scripts', 'mac');
60
+ // Tunables (kept here, not magic numbers)
61
+ const OSASCRIPT_TIMEOUT_MS = 8_000;
62
+ const A11Y_TREE_TIMEOUT_MS = 12_000;
63
+ const SCREENSHOT_TIMEOUT_MS = 10_000;
64
+ class MacOSAdapter {
65
+ platform = 'darwin';
66
+ screenSize = null;
67
+ screenshotHelperPath = null;
68
+ permissionCheckPath = null;
69
+ inputPermissionTested = false;
70
+ inputPermissionCached = false;
71
+ async init() {
72
+ // Find the helper binaries shipped with the app bundle.
73
+ this.screenshotHelperPath = this.findHelper('screenshot-helper');
74
+ this.permissionCheckPath = this.findHelper('permission-check');
75
+ // Pre-warm screen size so first capture is fast.
76
+ await this.getScreenSize().catch(() => null);
77
+ }
78
+ async shutdown() {
79
+ // No long-lived processes to clean up — osascript and nut-js are stateless.
80
+ }
81
+ // ─── PERMISSIONS ──────────────────────────────────────────────────
82
+ async checkPermissions() {
83
+ // Test what THIS process tree can actually do, not the app bundle's TCC entry.
84
+ const [accessibility, screenRecording] = await Promise.all([
85
+ this.probeAccessibility(),
86
+ this.probeScreenRecording(),
87
+ ]);
88
+ return { input: accessibility, accessibility, screenRecording };
89
+ }
90
+ async requestPermissions() {
91
+ // Spawn permission-check with prompt flags to trigger the native dialogs.
92
+ if (this.permissionCheckPath && fs.existsSync(this.permissionCheckPath)) {
93
+ try {
94
+ await execFileAsync(this.permissionCheckPath, ['--prompt', '--request-screen-recording'], { timeout: 30_000 });
95
+ }
96
+ catch {
97
+ // The binary may exit non-zero if perms denied — that's fine, dialogs were shown.
98
+ }
99
+ }
100
+ return this.checkPermissions();
101
+ }
102
+ async probeAccessibility() {
103
+ try {
104
+ // Must test actual window access — process enumeration works without assistive access.
105
+ await execFileAsync('osascript', ['-l', 'JavaScript', '-e',
106
+ 'var se = Application("System Events"); var p = se.processes.whose({frontmost: true})[0]; p.windows.length; true',
107
+ ], { timeout: 5_000 });
108
+ return true;
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ }
114
+ async probeScreenRecording() {
115
+ // Use the Swift screenshot-helper as the probe — it's what ClawdCursor actually uses.
116
+ if (!this.screenshotHelperPath || !fs.existsSync(this.screenshotHelperPath))
117
+ return false;
118
+ return new Promise((resolve) => {
119
+ const tmp = `/tmp/.clawdcursor-probe-${process.pid}.png`;
120
+ const proc = (0, child_process_1.spawn)(this.screenshotHelperPath, ['--fullscreen', tmp]);
121
+ const timer = setTimeout(() => { proc.kill(); resolve(false); }, 5_000);
122
+ proc.on('close', (code) => {
123
+ clearTimeout(timer);
124
+ try {
125
+ if (code === 0 && fs.existsSync(tmp) && fs.statSync(tmp).size > 100) {
126
+ fs.unlinkSync(tmp);
127
+ return resolve(true);
128
+ }
129
+ }
130
+ catch { /* fallthrough */ }
131
+ try {
132
+ fs.unlinkSync(tmp);
133
+ }
134
+ catch { /* */ }
135
+ resolve(false);
136
+ });
137
+ });
138
+ }
139
+ // ─── DISPLAY ──────────────────────────────────────────────────────
140
+ async getScreenSize() {
141
+ if (this.screenSize)
142
+ return this.screenSize;
143
+ // Query NSScreen for logical dimensions.
144
+ let logicalWidth = 0, logicalHeight = 0;
145
+ try {
146
+ const out = await execFileAsync('osascript', ['-e',
147
+ `use framework "AppKit"\n` +
148
+ `set frame to current application's NSScreen's mainScreen's frame()\n` +
149
+ `set sz to size of frame\n` +
150
+ `return ((width of sz) as integer) & "," & ((height of sz) as integer) as text`,
151
+ ], { timeout: 5_000 });
152
+ const [w, h] = out.stdout.trim().split(',').map(s => parseInt(s, 10));
153
+ logicalWidth = w || 0;
154
+ logicalHeight = h || 0;
155
+ }
156
+ catch { /* fall through */ }
157
+ // Capture a probe screenshot to learn physical (retina) dimensions.
158
+ // If the screenshot-helper binary is missing (e.g. published npm install
159
+ // before `cd native && ./build.sh`), degrade gracefully: assume 1x DPI
160
+ // so that listDisplays() and the a11y pipeline still work. callers that
161
+ // genuinely need screenshots will get the actionable error from screenshot().
162
+ let physicalWidth = logicalWidth;
163
+ let physicalHeight = logicalHeight;
164
+ let dpiRatio = 1;
165
+ try {
166
+ const probe = await this.screenshot();
167
+ physicalWidth = probe.width * probe.scaleFactor;
168
+ physicalHeight = probe.height * probe.scaleFactor;
169
+ if (!logicalWidth)
170
+ logicalWidth = probe.width;
171
+ if (!logicalHeight)
172
+ logicalHeight = probe.height;
173
+ dpiRatio = physicalWidth > logicalWidth ? physicalWidth / logicalWidth : 1;
174
+ }
175
+ catch (err) {
176
+ // Screenshot-helper absent or Screen Recording not granted.
177
+ // logicalWidth/logicalHeight may already be set from NSScreen above;
178
+ // if not, we have no dimensions at all — surface a clear error.
179
+ if (!logicalWidth || !logicalHeight) {
180
+ throw err; // re-throw so init()'s .catch() swallows it, not an opaque crash
181
+ }
182
+ // Usable logical dims from NSScreen — keep the physical = logical / 1x
183
+ // fallback set at declaration (screenshot() is the first statement in the
184
+ // try, so on this path physicalWidth/Height still hold the logical dims).
185
+ }
186
+ this.screenSize = { physicalWidth, physicalHeight, logicalWidth, logicalHeight, dpiRatio };
187
+ return this.screenSize;
188
+ }
189
+ async listDisplays() {
190
+ try {
191
+ const out = await execFileAsync('osascript', ['-e',
192
+ `use framework "AppKit"\n` +
193
+ `set output to ""\n` +
194
+ `set idx to 0\n` +
195
+ `set primaryScreen to current application's NSScreen's mainScreen()\n` +
196
+ `repeat with s in (current application's NSScreen's screens())\n` +
197
+ ` set frame to frame of s\n` +
198
+ ` set isPrimary to (s as any) = primaryScreen\n` +
199
+ ` set output to output & (idx as text) & "|" & (item 1 of item 1 of frame) & "," & (item 2 of item 1 of frame) & "," & (item 1 of item 2 of frame) & "," & (item 2 of item 2 of frame) & "|" & (isPrimary as text) & linefeed\n` +
200
+ ` set idx to idx + 1\n` +
201
+ `end repeat\n` +
202
+ `return output`,
203
+ ], { timeout: 5_000 });
204
+ const lines = out.stdout.trim().split('\n').filter(Boolean);
205
+ const primary = await this.getScreenSize();
206
+ return lines.map((line, i) => {
207
+ const [idx, bounds, primaryFlag] = line.split('|');
208
+ const [x, y, w, h] = bounds.split(',').map(v => parseInt(v, 10) || 0);
209
+ return {
210
+ index: parseInt(idx, 10) || i,
211
+ label: parseInt(idx, 10) === 0 ? 'Built-in Display' : `Display ${i + 1}`,
212
+ primary: primaryFlag.trim() === 'true',
213
+ bounds: { x, y, width: w, height: h },
214
+ physicalSize: {
215
+ width: Math.round(w * primary.dpiRatio),
216
+ height: Math.round(h * primary.dpiRatio),
217
+ },
218
+ dpiRatio: primary.dpiRatio,
219
+ };
220
+ });
221
+ }
222
+ catch {
223
+ const size = await this.getScreenSize();
224
+ return [{
225
+ index: 0,
226
+ label: 'Built-in Display',
227
+ primary: true,
228
+ bounds: { x: 0, y: 0, width: size.logicalWidth, height: size.logicalHeight },
229
+ physicalSize: { width: size.physicalWidth, height: size.physicalHeight },
230
+ dpiRatio: size.dpiRatio,
231
+ }];
232
+ }
233
+ }
234
+ async screenshot(opts) {
235
+ if (!this.screenshotHelperPath) {
236
+ // Build a human-readable list of the candidate paths that were searched
237
+ // so the user knows exactly where the binary was expected.
238
+ const root = (0, paths_1.getPackageRoot)();
239
+ const searched = [
240
+ path.join(root, 'native', 'ClawdCursor.app', 'Contents', 'MacOS', 'screenshot-helper'),
241
+ path.join(root, 'node_modules', '.clawdcursor', 'ClawdCursor.app', 'Contents', 'MacOS', 'screenshot-helper'),
242
+ path.join(os.homedir(), '.clawdcursor', 'ClawdCursor.app', 'Contents', 'MacOS', 'screenshot-helper'),
243
+ ].join('\n ');
244
+ throw new Error(`screenshot-helper not found. The native macOS helper binary was not built or installed.\n` +
245
+ `Searched:\n ${searched}\n\n` +
246
+ `To fix: cd into the clawdcursor package root and run:\n` +
247
+ ` cd native && ./build.sh\n` +
248
+ `(Requires Xcode Command Line Tools — install with: xcode-select --install)\n\n` +
249
+ `Or reinstall clawdcursor after installing the Command Line Tools:\n` +
250
+ ` npm install -g clawdcursor`);
251
+ }
252
+ const tmp = `/tmp/.clawdcursor-shot-${process.pid}-${Date.now()}.png`;
253
+ try {
254
+ // The Swift helper always captures the full screen. For non-primary
255
+ // display selection we crop post-hoc from listDisplays bounds.
256
+ await execFileAsync(this.screenshotHelperPath, ['--fullscreen', tmp], {
257
+ timeout: SCREENSHOT_TIMEOUT_MS,
258
+ });
259
+ let buffer = fs.readFileSync(tmp);
260
+ try {
261
+ fs.unlinkSync(tmp);
262
+ }
263
+ catch { /* */ }
264
+ const meta = await (0, sharp_1.default)(buffer).metadata();
265
+ let width = meta.width || 0;
266
+ let height = meta.height || 0;
267
+ let scaleFactor = 1;
268
+ if (opts?.displayIndex !== undefined && opts.displayIndex > 0) {
269
+ const displays = await this.listDisplays();
270
+ const target = displays[opts.displayIndex];
271
+ if (target) {
272
+ const r = target.dpiRatio || 1;
273
+ const left = Math.max(0, Math.round(target.bounds.x * r));
274
+ const top = Math.max(0, Math.round(target.bounds.y * r));
275
+ const w = Math.max(1, Math.min(Math.round(target.bounds.width * r), width - left));
276
+ const h = Math.max(1, Math.min(Math.round(target.bounds.height * r), height - top));
277
+ buffer = await (0, sharp_1.default)(buffer).extract({ left, top, width: w, height: h }).png().toBuffer();
278
+ width = w;
279
+ height = h;
280
+ }
281
+ }
282
+ if (opts?.maxWidth && width > opts.maxWidth) {
283
+ scaleFactor = width / opts.maxWidth;
284
+ const newH = Math.round(height / scaleFactor);
285
+ const resized = await (0, sharp_1.default)(buffer).resize(opts.maxWidth, newH, { fit: 'fill' }).png().toBuffer();
286
+ buffer = Buffer.from(resized);
287
+ width = opts.maxWidth;
288
+ height = newH;
289
+ }
290
+ return { buffer, width, height, scaleFactor };
291
+ }
292
+ catch (err) {
293
+ try {
294
+ fs.unlinkSync(tmp);
295
+ }
296
+ catch { /* */ }
297
+ throw err;
298
+ }
299
+ }
300
+ async screenshotRegion(x, y, w, h) {
301
+ const full = await this.screenshot();
302
+ const buffer = await (0, sharp_1.default)(full.buffer).extract({ left: x, top: y, width: w, height: h }).png().toBuffer();
303
+ return { buffer, width: w, height: h, scaleFactor: 1 };
304
+ }
305
+ // ─── WINDOWS ──────────────────────────────────────────────────────
306
+ async listWindows() {
307
+ try {
308
+ const scriptPath = path.join(SCRIPTS_DIR, 'get-windows.jxa');
309
+ const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', scriptPath], {
310
+ timeout: OSASCRIPT_TIMEOUT_MS,
311
+ });
312
+ const raw = JSON.parse(stdout);
313
+ return Array.isArray(raw) ? raw.map(this.normalizeWindow) : [];
314
+ }
315
+ catch {
316
+ return [];
317
+ }
318
+ }
319
+ async getActiveWindow() {
320
+ try {
321
+ const scriptPath = path.join(SCRIPTS_DIR, 'get-foreground-window.jxa');
322
+ const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', scriptPath], {
323
+ timeout: OSASCRIPT_TIMEOUT_MS,
324
+ });
325
+ const raw = JSON.parse(stdout);
326
+ return raw ? this.normalizeWindow(raw) : null;
327
+ }
328
+ catch {
329
+ return null;
330
+ }
331
+ }
332
+ async focusWindow(query) {
333
+ // focus-window.jxa accepts -processId <N> and/or -title <string>.
334
+ // The old implementation passed -FocusedProcessId / -ProcessName / -WindowTitle
335
+ // which the JXA run(argv) parser never matched, so ANY processId-only or
336
+ // processName-only query silently fell to "no match" and returned false.
337
+ //
338
+ // Resolution strategy (matches Windows adapter behaviour):
339
+ // 1. If processId is given, use it directly.
340
+ // 2. Else if processName is given, resolve it to a processId via listWindows
341
+ // (avoids conflating process names with window titles — e.g. "Safari" is
342
+ // process name "Safari" but window title could be "Apple - iPhone 16").
343
+ // 3. Pass title as the disambiguation hint when it is also provided.
344
+ let processId = query.processId;
345
+ if (processId === undefined && query.processName) {
346
+ const target = query.processName.toLowerCase();
347
+ const windows = await this.listWindows();
348
+ const hit = windows.find(w => w.processName.toLowerCase() === target)
349
+ ?? windows.find(w => w.processName.toLowerCase().includes(target));
350
+ if (hit)
351
+ processId = hit.processId;
352
+ }
353
+ const args = [];
354
+ if (processId !== undefined)
355
+ args.push('-processId', String(processId));
356
+ if (query.title)
357
+ args.push('-title', query.title);
358
+ if (args.length === 0)
359
+ return false; // nothing to match on
360
+ try {
361
+ const scriptPath = path.join(SCRIPTS_DIR, 'focus-window.jxa');
362
+ const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', scriptPath, ...args], {
363
+ timeout: OSASCRIPT_TIMEOUT_MS,
364
+ });
365
+ const result = JSON.parse(stdout);
366
+ return result?.success === true;
367
+ }
368
+ catch {
369
+ return false;
370
+ }
371
+ }
372
+ async maximizeWindow() {
373
+ // macOS native fullscreen toggle.
374
+ await this.keyPress('ctrl+mod+f').catch(() => { });
375
+ }
376
+ async setWindowState(state, query) {
377
+ // Resolve target window. When query is omitted, we target the frontmost
378
+ // app's frontmost window via System Events.
379
+ const targetClause = this.buildMacWindowTargetClause(query);
380
+ try {
381
+ let script;
382
+ if (state === 'close') {
383
+ // AX close action on the target window — matches UIA WindowPattern.Close semantics.
384
+ script = `tell application "System Events" to tell ${targetClause} to click (first button whose subrole is "AXCloseButton")`;
385
+ }
386
+ else if (state === 'minimize') {
387
+ script = `tell application "System Events" to tell ${targetClause} to set value of attribute "AXMinimized" to true`;
388
+ }
389
+ else if (state === 'maximize') {
390
+ // macOS's closest equivalent is the zoom button (green traffic light).
391
+ script = `tell application "System Events" to tell ${targetClause} to click (first button whose subrole is "AXZoomButton")`;
392
+ }
393
+ else {
394
+ // normal: restore from minimized (maximize toggle is an app-level choice on mac).
395
+ script = `tell application "System Events" to tell ${targetClause} to set value of attribute "AXMinimized" to false`;
396
+ }
397
+ await execFileAsync('osascript', ['-e', script], { timeout: OSASCRIPT_TIMEOUT_MS });
398
+ return true;
399
+ }
400
+ catch {
401
+ return false;
402
+ }
403
+ }
404
+ async setWindowBounds(bounds, query) {
405
+ const targetClause = this.buildMacWindowTargetClause(query);
406
+ try {
407
+ // Read current bounds for fields not supplied, then assign AXPosition + AXSize.
408
+ const readScript = `tell application "System Events" to tell ${targetClause}\n` +
409
+ ` set p to position\n` +
410
+ ` set s to size\n` +
411
+ ` return (item 1 of p as text) & "," & (item 2 of p as text) & "," & (item 1 of s as text) & "," & (item 2 of s as text)\n` +
412
+ `end tell`;
413
+ const { stdout: cur } = await execFileAsync('osascript', ['-e', readScript], {
414
+ timeout: OSASCRIPT_TIMEOUT_MS,
415
+ });
416
+ const [cx, cy, cw, ch] = cur.trim().split(',').map(s => parseInt(s, 10) || 0);
417
+ const x = bounds.x ?? cx;
418
+ const y = bounds.y ?? cy;
419
+ const w = bounds.width ?? cw;
420
+ const h = bounds.height ?? ch;
421
+ const setScript = `tell application "System Events" to tell ${targetClause}\n` +
422
+ ` set position to {${x}, ${y}}\n` +
423
+ ` set size to {${w}, ${h}}\n` +
424
+ `end tell`;
425
+ await execFileAsync('osascript', ['-e', setScript], { timeout: OSASCRIPT_TIMEOUT_MS });
426
+ return true;
427
+ }
428
+ catch {
429
+ return false;
430
+ }
431
+ }
432
+ buildMacWindowTargetClause(query) {
433
+ if (!query)
434
+ return 'window 1 of (first application process whose frontmost is true)';
435
+ if (query.processName) {
436
+ const safe = query.processName.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
437
+ if (query.title) {
438
+ const t = query.title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
439
+ return `window "${t}" of application process "${safe}"`;
440
+ }
441
+ return `window 1 of application process "${safe}"`;
442
+ }
443
+ if (query.processId !== undefined) {
444
+ if (query.title) {
445
+ const t = query.title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
446
+ return `window "${t}" of (first application process whose unix id is ${query.processId})`;
447
+ }
448
+ return `window 1 of (first application process whose unix id is ${query.processId})`;
449
+ }
450
+ if (query.title) {
451
+ const t = query.title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
452
+ return `first window whose title contains "${t}" of (first application process whose frontmost is true)`;
453
+ }
454
+ return 'window 1 of (first application process whose frontmost is true)';
455
+ }
456
+ // ─── ACCESSIBILITY ────────────────────────────────────────────────
457
+ async getUiTree(processId) {
458
+ try {
459
+ const args = ['-l', 'JavaScript', path.join(SCRIPTS_DIR, 'get-screen-context.jxa')];
460
+ // Match the Windows adapter's depth (8). The JXA default is 2, which only
461
+ // reaches window → group → child — far too shallow for real apps (Mail,
462
+ // Safari chrome, System Settings nest controls deeper), so compile_ui/
463
+ // el_NN saw a near-empty tree and named targets went missing on macOS
464
+ // (audit 2026-06-11, M4). The legacy AccessibilityManager already drives
465
+ // this same script at depth 8.
466
+ const scriptArgs = ['-MaxDepth', '8'];
467
+ // Default to the frontmost app's pid when the caller omits it, so an
468
+ // unscoped read_screen targets the active window (parity with the Windows
469
+ // adapter) instead of relying on the script's default scope.
470
+ let pid = processId;
471
+ if (pid === undefined) {
472
+ const fg = await this.getActiveWindow().catch(() => null);
473
+ if (fg?.processId)
474
+ pid = fg.processId;
475
+ }
476
+ if (pid !== undefined)
477
+ scriptArgs.push('-FocusedProcessId', String(pid));
478
+ args.push('--', ...scriptArgs);
479
+ const { stdout } = await execFileAsync('osascript', args, { timeout: A11Y_TREE_TIMEOUT_MS });
480
+ const data = JSON.parse(stdout);
481
+ return this.flattenTree(data?.uiTree);
482
+ }
483
+ catch {
484
+ return [];
485
+ }
486
+ }
487
+ async findElements(query) {
488
+ try {
489
+ const args = ['-l', 'JavaScript', path.join(SCRIPTS_DIR, 'find-element.jxa'), '--'];
490
+ // find-element.jxa parses '-ProcessId' (NOT '-FocusedProcessId' — that name
491
+ // is only understood by get-screen-context.jxa). Passing the wrong flag
492
+ // silently dropped pid scoping (review 2026-06-11).
493
+ if (query.processId !== undefined)
494
+ args.push('-ProcessId', String(query.processId));
495
+ if (query.name)
496
+ args.push('-Name', query.name);
497
+ if (query.controlType)
498
+ args.push('-ControlType', query.controlType);
499
+ const { stdout } = await execFileAsync('osascript', args, { timeout: A11Y_TREE_TIMEOUT_MS });
500
+ const raw = JSON.parse(stdout);
501
+ return Array.isArray(raw) ? raw.map(this.normalizeElement) : [];
502
+ }
503
+ catch {
504
+ return [];
505
+ }
506
+ }
507
+ async getFocusedElement() {
508
+ try {
509
+ const scriptPath = path.join(SCRIPTS_DIR, 'get-focused-element.jxa');
510
+ const { stdout } = await execFileAsync('osascript', ['-l', 'JavaScript', scriptPath], {
511
+ timeout: OSASCRIPT_TIMEOUT_MS,
512
+ });
513
+ const raw = JSON.parse(stdout);
514
+ return raw ? this.normalizeElement(raw) : null;
515
+ }
516
+ catch {
517
+ return null;
518
+ }
519
+ }
520
+ async invokeElement(query) {
521
+ try {
522
+ const args = ['-l', 'JavaScript', path.join(SCRIPTS_DIR, 'invoke-element.jxa'), '--'];
523
+ // invoke-element.jxa parses '-ProcessId' (NOT '-FocusedProcessId' — that
524
+ // name belongs to get-screen-context.jxa). The wrong flag made the JXA
525
+ // fail its required-processId check, breaking every pid-scoped invoke /
526
+ // get-value on macOS (review 2026-06-11). The '--' is always present so
527
+ // later flags are never eaten by osascript when processId is omitted.
528
+ if (query.processId !== undefined)
529
+ args.push('-ProcessId', String(query.processId));
530
+ if (query.name)
531
+ args.push('-Name', query.name);
532
+ if (query.controlType)
533
+ args.push('-ControlType', query.controlType);
534
+ if (query.action)
535
+ args.push('-Action', query.action);
536
+ if (query.value !== undefined)
537
+ args.push('-Value', query.value);
538
+ const { stdout } = await execFileAsync('osascript', args, { timeout: OSASCRIPT_TIMEOUT_MS });
539
+ const result = JSON.parse(stdout);
540
+ return {
541
+ success: result?.success === true,
542
+ bounds: result?.bounds,
543
+ // get-value returns its payload at the TOP level ({success, action,
544
+ // value, method}); consumers read res.data?.value — surface it
545
+ // (review 2026-06-11; parity with the Windows adapter).
546
+ data: result?.data ?? (result?.value !== undefined ? { value: result.value } : undefined),
547
+ };
548
+ }
549
+ catch {
550
+ return { success: false };
551
+ }
552
+ }
553
+ async waitForElement(query, timeoutMs) {
554
+ const interval = query.intervalMs ?? 250;
555
+ const deadline = Date.now() + timeoutMs;
556
+ while (Date.now() < deadline) {
557
+ const hits = await this.findElements({
558
+ name: query.name,
559
+ controlType: query.controlType,
560
+ processId: query.processId,
561
+ });
562
+ if (hits.length > 0)
563
+ return hits[0];
564
+ await this.delay(interval);
565
+ }
566
+ return null;
567
+ }
568
+ // ─── INPUT (mouse) ────────────────────────────────────────────────
569
+ lastCursor = null;
570
+ toNutButton(button) {
571
+ if (button === 'right')
572
+ return nut_js_1.Button.RIGHT;
573
+ if (button === 'middle')
574
+ return nut_js_1.Button.MIDDLE;
575
+ return nut_js_1.Button.LEFT;
576
+ }
577
+ async mouseClick(x, y, opts) {
578
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
579
+ this.lastCursor = { x, y };
580
+ await this.delay(40);
581
+ const count = opts?.count ?? 1;
582
+ const btn = this.toNutButton(opts?.button);
583
+ for (let i = 0; i < count; i++) {
584
+ if (btn === nut_js_1.Button.RIGHT)
585
+ await nut_js_1.mouse.rightClick();
586
+ else if (btn === nut_js_1.Button.MIDDLE) {
587
+ await nut_js_1.mouse.pressButton(nut_js_1.Button.MIDDLE);
588
+ await this.delay(30);
589
+ await nut_js_1.mouse.releaseButton(nut_js_1.Button.MIDDLE);
590
+ }
591
+ else {
592
+ await nut_js_1.mouse.click(nut_js_1.Button.LEFT);
593
+ }
594
+ if (i < count - 1)
595
+ await this.delay(60);
596
+ }
597
+ }
598
+ async mouseMove(x, y) {
599
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
600
+ this.lastCursor = { x, y };
601
+ }
602
+ async mouseMoveRelative(dx, dy) {
603
+ // nut-js getPosition() works reliably on macOS.
604
+ try {
605
+ const pos = await nut_js_1.mouse.getPosition();
606
+ const nx = Math.round(pos.x + dx);
607
+ const ny = Math.round(pos.y + dy);
608
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
609
+ this.lastCursor = { x: nx, y: ny };
610
+ }
611
+ catch {
612
+ if (this.lastCursor) {
613
+ const nx = this.lastCursor.x + dx;
614
+ const ny = this.lastCursor.y + dy;
615
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
616
+ this.lastCursor = { x: nx, y: ny };
617
+ }
618
+ }
619
+ }
620
+ async mouseDrag(x1, y1, x2, y2) {
621
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x1, y1));
622
+ this.lastCursor = { x: x1, y: y1 };
623
+ await this.delay(50);
624
+ await nut_js_1.mouse.pressButton(nut_js_1.Button.LEFT);
625
+ await this.delay(80);
626
+ const steps = Math.max(8, Math.floor(Math.hypot(x2 - x1, y2 - y1) / 18));
627
+ for (let i = 1; i <= steps; i++) {
628
+ const t = i / steps;
629
+ const nx = Math.round(x1 + (x2 - x1) * t);
630
+ const ny = Math.round(y1 + (y2 - y1) * t);
631
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(nx, ny));
632
+ this.lastCursor = { x: nx, y: ny };
633
+ await this.delay(10);
634
+ }
635
+ await nut_js_1.mouse.releaseButton(nut_js_1.Button.LEFT);
636
+ }
637
+ async mouseScroll(x, y, direction, amount = 3) {
638
+ await nut_js_1.mouse.setPosition(new nut_js_1.Point(x, y));
639
+ this.lastCursor = { x, y };
640
+ await this.delay(30);
641
+ if (direction === 'down')
642
+ await nut_js_1.mouse.scrollDown(amount);
643
+ else if (direction === 'up')
644
+ await nut_js_1.mouse.scrollUp(amount);
645
+ else {
646
+ // macOS horizontal scroll — hold Shift and scroll vertically. Most
647
+ // apps interpret Shift+wheel as horizontal.
648
+ const shiftScript = direction === 'left'
649
+ ? 'tell application "System Events" to key down shift'
650
+ : 'tell application "System Events" to key down shift';
651
+ await execFileAsync('osascript', ['-e', shiftScript], { timeout: 2_000 }).catch(() => { });
652
+ try {
653
+ if (direction === 'left')
654
+ await nut_js_1.mouse.scrollUp(amount);
655
+ else
656
+ await nut_js_1.mouse.scrollDown(amount);
657
+ }
658
+ finally {
659
+ await execFileAsync('osascript', ['-e',
660
+ 'tell application "System Events" to key up shift',
661
+ ], { timeout: 2_000 }).catch(() => { });
662
+ }
663
+ }
664
+ }
665
+ async mouseDown(button) {
666
+ await nut_js_1.mouse.pressButton(this.toNutButton(button));
667
+ }
668
+ async mouseUp(button) {
669
+ await nut_js_1.mouse.releaseButton(this.toNutButton(button));
670
+ }
671
+ // ─── INPUT (keyboard) ─────────────────────────────────────────────
672
+ async typeText(text) {
673
+ if (!text)
674
+ return;
675
+ // Escape backslashes and quotes for AppleScript string literal.
676
+ const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
677
+ await execFileAsync('osascript', ['-e',
678
+ `tell application "System Events" to keystroke "${escaped}"`,
679
+ ], { timeout: 15_000 });
680
+ }
681
+ async keyPress(combo) {
682
+ if (!combo)
683
+ return;
684
+ // Literal "+" can't be split.
685
+ if (combo === '+') {
686
+ await execFileAsync('osascript', ['-e', 'tell application "System Events" to keystroke "+"']);
687
+ return;
688
+ }
689
+ const parts = combo.split('+').map(s => s.trim()).filter(Boolean);
690
+ const key = parts[parts.length - 1];
691
+ const mods = parts.slice(0, -1).map(this.normalizeMod);
692
+ const usingClause = mods.length ? ` using {${mods.map(m => `${m} down`).join(', ')}}` : '';
693
+ // Special keys → key code; printable single chars → keystroke.
694
+ const keyCode = MAC_KEY_CODES[key.toLowerCase()];
695
+ let script;
696
+ if (keyCode !== undefined) {
697
+ script = `tell application "System Events" to key code ${keyCode}${usingClause}`;
698
+ }
699
+ else if (key.length === 1) {
700
+ const escaped = key.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
701
+ script = `tell application "System Events" to keystroke "${escaped}"${usingClause}`;
702
+ }
703
+ else {
704
+ // Unknown multi-char key — try as keystroke (covers things like word chars).
705
+ script = `tell application "System Events" to keystroke "${key}"${usingClause}`;
706
+ }
707
+ await execFileAsync('osascript', ['-e', script], { timeout: 6_000 });
708
+ }
709
+ normalizeMod(mod) {
710
+ const m = mod.toLowerCase();
711
+ if (m === 'mod' || m === 'cmd' || m === 'command' || m === 'super' || m === 'meta')
712
+ return 'command';
713
+ if (m === 'shift')
714
+ return 'shift';
715
+ if (m === 'alt' || m === 'option' || m === 'opt')
716
+ return 'option';
717
+ if (m === 'ctrl' || m === 'control')
718
+ return 'control';
719
+ return m;
720
+ }
721
+ async keyDown(key) {
722
+ // macOS key down/up via System Events. Supports named modifiers
723
+ // (shift/option/control/command) and arbitrary key codes. Non-modifier
724
+ // single chars fall back to a brief keystroke.
725
+ const lower = key.trim().toLowerCase();
726
+ const asMod = this.modToAppleScript(lower);
727
+ if (asMod) {
728
+ const script = `tell application "System Events" to key down ${asMod}`;
729
+ await execFileAsync('osascript', ['-e', script], { timeout: 3_000 }).catch(() => { });
730
+ return;
731
+ }
732
+ const code = MAC_KEY_CODES[lower];
733
+ if (code !== undefined) {
734
+ const script = `tell application "System Events" to key down (key code ${code})`;
735
+ await execFileAsync('osascript', ['-e', script], { timeout: 3_000 }).catch(() => { });
736
+ return;
737
+ }
738
+ // Printable single char — no true "hold" semantics via keystroke; emit a tap.
739
+ if (key.length === 1) {
740
+ const escaped = key.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
741
+ const script = `tell application "System Events" to keystroke "${escaped}"`;
742
+ await execFileAsync('osascript', ['-e', script], { timeout: 3_000 }).catch(() => { });
743
+ }
744
+ }
745
+ async keyUp(key) {
746
+ const lower = key.trim().toLowerCase();
747
+ const asMod = this.modToAppleScript(lower);
748
+ if (asMod) {
749
+ const script = `tell application "System Events" to key up ${asMod}`;
750
+ await execFileAsync('osascript', ['-e', script], { timeout: 3_000 }).catch(() => { });
751
+ return;
752
+ }
753
+ const code = MAC_KEY_CODES[lower];
754
+ if (code !== undefined) {
755
+ const script = `tell application "System Events" to key up (key code ${code})`;
756
+ await execFileAsync('osascript', ['-e', script], { timeout: 3_000 }).catch(() => { });
757
+ }
758
+ // Printable single char: no-op — keystroke doesn't hold.
759
+ }
760
+ modToAppleScript(name) {
761
+ if (name === 'mod' || name === 'cmd' || name === 'command' || name === 'meta' || name === 'super')
762
+ return 'command';
763
+ if (name === 'shift')
764
+ return 'shift';
765
+ if (name === 'alt' || name === 'option' || name === 'opt')
766
+ return 'option';
767
+ if (name === 'ctrl' || name === 'control')
768
+ return 'control';
769
+ return null;
770
+ }
771
+ // ─── CLIPBOARD ────────────────────────────────────────────────────
772
+ async readClipboard() {
773
+ try {
774
+ const { stdout } = await execFileAsync('pbpaste', [], { timeout: 3_000 });
775
+ return stdout;
776
+ }
777
+ catch {
778
+ return '';
779
+ }
780
+ }
781
+ async writeClipboard(text) {
782
+ return new Promise((resolve, reject) => {
783
+ const proc = (0, child_process_1.spawn)('pbcopy');
784
+ const timer = setTimeout(() => { proc.kill(); reject(new Error('pbcopy timeout')); }, 3_000);
785
+ proc.on('close', (code) => {
786
+ clearTimeout(timer);
787
+ if (code === 0)
788
+ resolve();
789
+ else
790
+ reject(new Error(`pbcopy exit ${code}`));
791
+ });
792
+ proc.stdin.write(text);
793
+ proc.stdin.end();
794
+ });
795
+ }
796
+ // ─── APPS ─────────────────────────────────────────────────────────
797
+ /**
798
+ * Thin shim — delegates straight to `launchApp` with no alias resolution.
799
+ * The platform layer is alias-data-agnostic; cross-OS name mapping (e.g.
800
+ * Windows "Notepad" → mac "TextEdit") happens in the caller above (the
801
+ * agent's `open_app` tool, the router's `handleOpenApp`). Callers that
802
+ * want bundle-name / searchTerm hints must pass them via `launchApp`.
803
+ */
804
+ async openApp(name, opts) {
805
+ return this.launchApp(name, opts);
806
+ }
807
+ async launchApp(name, opts) {
808
+ // uwpAppId is Windows-only — ignore on macOS.
809
+ void opts?.uwpAppId;
810
+ // Reject shell-metachar input even though we use execFile (no shell expansion).
811
+ // Keeps parity with Windows' stricter validator and avoids surprising `open`.
812
+ if (/[\r\n\t\x00-\x1f]/.test(name)) {
813
+ throw new Error('launchApp: illegal characters in app name');
814
+ }
815
+ // Snapshot windows BEFORE any spawn so the post-launch diff-and-poll
816
+ // helper can ignore them. Reused for the idempotency check below to
817
+ // save a redundant Apple Events round-trip.
818
+ let windowsBefore = [];
819
+ try {
820
+ windowsBefore = await this.listWindows();
821
+ }
822
+ catch {
823
+ // Non-fatal — empty before-set is a safe default.
824
+ }
825
+ // v0.8.3 — idempotency. On macOS `open -a AppName` is generally smart
826
+ // about not spawning duplicates (it activates the existing app), but
827
+ // we want a stable cross-OS contract: check first, focus-if-running,
828
+ // launch only when needed. Prevents the "Outlook keeps opening" class
829
+ // of bug from any retry loop in the pipeline.
830
+ if (!opts?.alwaysNewInstance && !opts?.url) {
831
+ const target = name.toLowerCase();
832
+ const existing = windowsBefore.find(w => w.processName.toLowerCase() === target ||
833
+ w.processName.toLowerCase().includes(target) ||
834
+ w.title.toLowerCase().includes(target));
835
+ if (existing) {
836
+ await this.focusWindow({ processId: existing.processId }).catch(() => { });
837
+ return { pid: existing.processId, title: existing.title, handle: existing.handle };
838
+ }
839
+ }
840
+ try {
841
+ const args = ['-a', name];
842
+ if (opts?.alwaysNewInstance)
843
+ args.unshift('-n');
844
+ if (opts?.url)
845
+ args.push(opts.url);
846
+ await execFileAsync('open', args, {
847
+ timeout: 5_000,
848
+ cwd: opts?.cwd,
849
+ });
850
+ // Diff-and-poll the window list with a tighter primary budget, so the
851
+ // Spotlight fallback below has time if `open -a` didn't surface a
852
+ // window (e.g., bundle name typo, name-not-found at App registry).
853
+ const win = await (0, launch_poll_1.waitForLaunchedWindow)(windowsBefore, () => this.listWindows(), (0, launch_poll_1.buildAppPredicate)(name), { timeoutMs: 4_000 });
854
+ if (win)
855
+ return { pid: win.processId, title: win.title, handle: win.handle };
856
+ }
857
+ catch {
858
+ // open -a failed outright — Spotlight fallback below is still worth
859
+ // trying for apps the user can find by name.
860
+ }
861
+ // Spotlight fallback — universal launcher for anything macOS can find.
862
+ // Mirrors the pattern Windows uses with the Start Menu and the same
863
+ // shape the router's zero-LLM fast path already proves. Keyboard goes
864
+ // through the platform's internal primitives; not gated by the safety
865
+ // layer because this is an internal launch detail.
866
+ return this.launchViaSpotlight(name, opts?.searchTerm, windowsBefore);
867
+ }
868
+ /**
869
+ * Spotlight-driven launch fallback. Cmd+Space, type, Return, then poll.
870
+ * The Cmd+Space combo is on the safety blocklist for agent-emitted keys,
871
+ * but here the platform is calling its own keyboard primitives directly
872
+ * to fulfill its own `launchApp` contract — the safety layer is for
873
+ * agent actions, not internal platform plumbing.
874
+ */
875
+ async launchViaSpotlight(name, searchTermHint, windowsBefore) {
876
+ // Prefer the alias's curated `searchTerm` (or `macOSAppName`); fall back
877
+ // to the stripped bundle name. Keeps the launcher app-agnostic — adding
878
+ // apps still means a row in `aliases.ts`, not platform code.
879
+ const searchText = (searchTermHint && searchTermHint.trim())
880
+ ? searchTermHint.trim()
881
+ : name.replace(/\.app$/i, '');
882
+ try {
883
+ await this.keyPress('Escape').catch(() => { });
884
+ await this.delay(120);
885
+ await this.keyPress('cmd+Space');
886
+ await this.delay(300);
887
+ // Strip a trailing `.app` so Spotlight ranks the bundle correctly —
888
+ // typing `Calculator.app` matches a Finder hit, `Calculator` matches
889
+ // the app itself. Already handled in `searchText` above.
890
+ await this.typeText(searchText);
891
+ await this.delay(500);
892
+ await this.keyPress('Return');
893
+ }
894
+ catch {
895
+ // Keyboard layer flaky — fall through to the empty result.
896
+ }
897
+ const win = await (0, launch_poll_1.waitForLaunchedWindow)(windowsBefore, () => this.listWindows(), (0, launch_poll_1.buildAppPredicate)(name), { timeoutMs: 4_000 });
898
+ return win
899
+ ? { pid: win.processId, title: win.title, handle: win.handle }
900
+ : {};
901
+ }
902
+ // ─── INTERNAL HELPERS ─────────────────────────────────────────────
903
+ findHelper(name) {
904
+ const root = (0, paths_1.getPackageRoot)();
905
+ const candidates = [
906
+ path.join(root, 'native', 'ClawdCursor.app', 'Contents', 'MacOS', name),
907
+ path.join(root, 'node_modules', '.clawdcursor', 'ClawdCursor.app', 'Contents', 'MacOS', name),
908
+ path.join(os.homedir(), '.clawdcursor', 'ClawdCursor.app', 'Contents', 'MacOS', name),
909
+ ];
910
+ return candidates.find(p => fs.existsSync(p)) ?? null;
911
+ }
912
+ normalizeWindow = (raw) => ({
913
+ title: raw.title ?? '',
914
+ processName: raw.processName ?? '',
915
+ processId: raw.processId ?? 0,
916
+ bounds: raw.bounds ?? { x: 0, y: 0, width: 0, height: 0 },
917
+ isMinimized: raw.isMinimized ?? false,
918
+ handle: raw.handle ?? raw.processId,
919
+ });
920
+ normalizeElement = (raw) => {
921
+ const enabled = raw.enabled;
922
+ // AX secure fields (subrole AXSecureTextField) carry secure=true from the
923
+ // JXA helper. Never surface their value — the helper already blanked it, but
924
+ // drop it here too so a future helper change can't leak it (M2).
925
+ const secure = raw.secure === true;
926
+ return {
927
+ name: raw.name ?? '',
928
+ controlType: (raw.controlType ?? '').replace('AX', ''),
929
+ subrole: raw.subrole,
930
+ secure,
931
+ bounds: raw.bounds ?? { x: 0, y: 0, width: 0, height: 0 },
932
+ value: secure ? undefined : raw.value,
933
+ enabled,
934
+ focused: raw.focused,
935
+ // Tranche 1A: state fields — the JXA helper surfaces these when set.
936
+ selected: raw.selected,
937
+ disabled: enabled === false ? true : undefined,
938
+ busy: raw.busy,
939
+ offscreen: raw.offscreen,
940
+ expandable: raw.expandable,
941
+ expanded: raw.expanded,
942
+ automationId: raw.identifier ?? raw.automationId,
943
+ processId: raw.processId,
944
+ };
945
+ };
946
+ flattenTree(node, acc = []) {
947
+ if (!node)
948
+ return acc;
949
+ if (node.controlType || node.name)
950
+ acc.push(this.normalizeElement(node));
951
+ if (Array.isArray(node.children)) {
952
+ for (const child of node.children)
953
+ this.flattenTree(child, acc);
954
+ }
955
+ return acc;
956
+ }
957
+ delay(ms) {
958
+ return new Promise(r => setTimeout(r, ms));
959
+ }
960
+ }
961
+ exports.MacOSAdapter = MacOSAdapter;
962
+ // macOS virtual keycodes for special keys (US ANSI keyboard).
963
+ const MAC_KEY_CODES = {
964
+ 'return': 36, 'enter': 36,
965
+ 'tab': 48,
966
+ 'space': 49,
967
+ 'delete': 51, 'backspace': 51,
968
+ 'escape': 53, 'esc': 53,
969
+ 'left': 123, 'right': 124, 'down': 125, 'up': 126,
970
+ 'home': 115, 'end': 119, 'pageup': 116, 'pagedown': 121,
971
+ 'f1': 122, 'f2': 120, 'f3': 99, 'f4': 118,
972
+ 'f5': 96, 'f6': 97, 'f7': 98, 'f8': 100,
973
+ 'f9': 101, 'f10': 109, 'f11': 103, 'f12': 111,
974
+ 'forwarddelete': 117,
975
+ };
976
+ //# sourceMappingURL=macos.js.map