agentix-cli 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +67 -118
  2. package/apps/mac-helper/Sources/AgentXHelper/AXTree.swift +159 -0
  3. package/apps/mac-helper/Sources/AgentXHelper/Brand.swift +63 -0
  4. package/apps/mac-helper/Sources/AgentXHelper/Focus.swift +97 -0
  5. package/apps/mac-helper/Sources/AgentXHelper/HUD.swift +179 -0
  6. package/apps/mac-helper/Sources/AgentXHelper/OCR.swift +117 -0
  7. package/apps/mac-helper/Sources/AgentXHelper/Pointer.swift +309 -0
  8. package/apps/mac-helper/Sources/AgentXHelper/Screens.swift +67 -0
  9. package/apps/mac-helper/Sources/AgentXHelper/Typer.swift +185 -0
  10. package/apps/mac-helper/Sources/AgentXHelper/Vision.swift +201 -0
  11. package/apps/mac-helper/Sources/AgentXHelper/main.swift +265 -0
  12. package/apps/mac-helper/build.sh +39 -0
  13. package/apps/mac-voice/Resources/Info.plist +18 -0
  14. package/apps/mac-voice/Sources/AgentXVoice/AgentClient.swift +108 -0
  15. package/apps/mac-voice/Sources/AgentXVoice/Brand.swift +123 -0
  16. package/apps/mac-voice/Sources/AgentXVoice/Config.swift +68 -0
  17. package/apps/mac-voice/Sources/AgentXVoice/Hold.swift +74 -0
  18. package/apps/mac-voice/Sources/AgentXVoice/Hotkey.swift +76 -0
  19. package/apps/mac-voice/Sources/AgentXVoice/Markdown.swift +140 -0
  20. package/apps/mac-voice/Sources/AgentXVoice/Panel.swift +275 -0
  21. package/apps/mac-voice/Sources/AgentXVoice/Progress.swift +147 -0
  22. package/apps/mac-voice/Sources/AgentXVoice/Recorder.swift +139 -0
  23. package/apps/mac-voice/Sources/AgentXVoice/ResultCard.swift +162 -0
  24. package/apps/mac-voice/Sources/AgentXVoice/SmartPaste.swift +72 -0
  25. package/apps/mac-voice/Sources/AgentXVoice/Speech.swift +192 -0
  26. package/apps/mac-voice/Sources/AgentXVoice/StepLabel.swift +159 -0
  27. package/apps/mac-voice/Sources/AgentXVoice/main.swift +380 -0
  28. package/apps/mac-voice/build.sh +32 -0
  29. package/dist/{App-BPNHIGUX.js → App-CKPH3HHF.js} +2 -2
  30. package/dist/{ChatApp-6XSSZB4U.js → ChatApp-MX7IG4K2.js} +2 -2
  31. package/dist/agent-2U574KBZ.js +2 -0
  32. package/dist/article-fields-L4D2BOGY.js +2 -0
  33. package/dist/article-quality-44ONVGLN.js +3 -0
  34. package/dist/article-quality-44ONVGLN.js.map +1 -0
  35. package/dist/{audit-JU3HWISB.js → audit-4245CX6P.js} +2 -2
  36. package/dist/backfill-MTD2B62I.js +6 -0
  37. package/dist/backfill-MTD2B62I.js.map +1 -0
  38. package/dist/{board-dashboard-M3372FLO.js → board-dashboard-QR7CVTKA.js} +1411 -453
  39. package/dist/board-dashboard-QR7CVTKA.js.map +1 -0
  40. package/dist/{builtin-FRKAQMFS.js → builtin-B67DMJ3M.js} +2 -2
  41. package/dist/calibration-ILS5OI44.js +2 -0
  42. package/dist/{chunk-V3U4OF7L.js → chunk-3LYM22IV.js} +46 -48
  43. package/dist/chunk-3LYM22IV.js.map +1 -0
  44. package/dist/chunk-3SCPRYJB.js +5 -0
  45. package/dist/chunk-3SCPRYJB.js.map +1 -0
  46. package/dist/chunk-4BR7D2OU.js +36 -0
  47. package/dist/chunk-4BR7D2OU.js.map +1 -0
  48. package/dist/{chunk-P4VWIVKN.js → chunk-52ZPELD2.js} +403 -29
  49. package/dist/chunk-52ZPELD2.js.map +1 -0
  50. package/dist/chunk-5IA2CYHN.js +5 -0
  51. package/dist/{chunk-O6YPEOQF.js.map → chunk-5IA2CYHN.js.map} +1 -1
  52. package/dist/chunk-64CAIB3C.js +9 -0
  53. package/dist/chunk-64CAIB3C.js.map +1 -0
  54. package/dist/{chunk-5U35WAZF.js → chunk-77OV7346.js} +6 -6
  55. package/dist/chunk-77OV7346.js.map +1 -0
  56. package/dist/chunk-AJ5RMU7X.js +2 -0
  57. package/dist/chunk-AJ5RMU7X.js.map +1 -0
  58. package/dist/chunk-AJONZWLT.js +2 -0
  59. package/dist/chunk-AJONZWLT.js.map +1 -0
  60. package/dist/chunk-BH3GTCZ6.js +2 -0
  61. package/dist/chunk-BH3GTCZ6.js.map +1 -0
  62. package/dist/chunk-E3UJHQGN.js +1189 -0
  63. package/dist/chunk-E3UJHQGN.js.map +1 -0
  64. package/dist/chunk-EVWEG3VD.js +3 -0
  65. package/dist/chunk-EVWEG3VD.js.map +1 -0
  66. package/dist/chunk-F4HZTZHW.js +2 -0
  67. package/dist/chunk-F4HZTZHW.js.map +1 -0
  68. package/dist/chunk-F6CNF7V3.js +25 -0
  69. package/dist/chunk-F6CNF7V3.js.map +1 -0
  70. package/dist/chunk-FIOB3MEX.js +2 -0
  71. package/dist/{chunk-H5D7ATXS.js → chunk-FKBGJYWQ.js} +5 -5
  72. package/dist/chunk-FKBGJYWQ.js.map +1 -0
  73. package/dist/{chunk-BVGJSPW4.js → chunk-G2HMRDUQ.js} +7 -7
  74. package/dist/chunk-G2HMRDUQ.js.map +1 -0
  75. package/dist/chunk-H2QQU2XV.js +109 -0
  76. package/dist/chunk-H2QQU2XV.js.map +1 -0
  77. package/dist/{chunk-LDHPMJ7M.js → chunk-IWMW5432.js} +3 -3
  78. package/dist/chunk-IWMW5432.js.map +1 -0
  79. package/dist/chunk-K33GIBSE.js +2 -0
  80. package/dist/chunk-K33GIBSE.js.map +1 -0
  81. package/dist/chunk-L2WSPY6S.js +2 -0
  82. package/dist/chunk-L2WSPY6S.js.map +1 -0
  83. package/dist/chunk-LCIREYTD.js +54 -0
  84. package/dist/chunk-LCIREYTD.js.map +1 -0
  85. package/dist/chunk-MHIMGM4G.js +126 -0
  86. package/dist/chunk-MHIMGM4G.js.map +1 -0
  87. package/dist/chunk-P5ZBBEPL.js +13 -0
  88. package/dist/chunk-P5ZBBEPL.js.map +1 -0
  89. package/dist/{chunk-JTJIPNXY.js → chunk-PNWNVOEZ.js} +1 -1
  90. package/dist/chunk-PNWNVOEZ.js.map +1 -0
  91. package/dist/chunk-QHTQPBHU.js +45 -0
  92. package/dist/chunk-QHTQPBHU.js.map +1 -0
  93. package/dist/chunk-S2MNL5V6.js +2 -0
  94. package/dist/chunk-S2MNL5V6.js.map +1 -0
  95. package/dist/chunk-SCTIUFL4.js +5 -0
  96. package/dist/{chunk-K2CAPYES.js.map → chunk-SCTIUFL4.js.map} +1 -1
  97. package/dist/chunk-ULLAE6LE.js +157 -0
  98. package/dist/chunk-ULLAE6LE.js.map +1 -0
  99. package/dist/{chunk-DJEZ2HVU.js → chunk-VDMYBPYB.js} +1 -1
  100. package/dist/chunk-VDMYBPYB.js.map +1 -0
  101. package/dist/chunk-VIH7ARPF.js +547 -0
  102. package/dist/chunk-VIH7ARPF.js.map +1 -0
  103. package/dist/chunk-W5C2GDNC.js +32 -0
  104. package/dist/{chunk-JP5IEXR7.js.map → chunk-W5C2GDNC.js.map} +1 -1
  105. package/dist/chunk-WPMX3VGI.js +3 -0
  106. package/dist/chunk-WPMX3VGI.js.map +1 -0
  107. package/dist/chunk-YLBERT7N.js +24 -0
  108. package/dist/chunk-YLBERT7N.js.map +1 -0
  109. package/dist/chunk-YLGIZRHV.js +114 -0
  110. package/dist/chunk-YLGIZRHV.js.map +1 -0
  111. package/dist/chunk-ZMYGXHPT.js +121 -0
  112. package/dist/chunk-ZMYGXHPT.js.map +1 -0
  113. package/dist/chunk-ZTEU5JVQ.js +2 -0
  114. package/dist/chunk-ZTEU5JVQ.js.map +1 -0
  115. package/dist/{chunk-2WVX3LEC.js → chunk-ZVP6GB4G.js} +1 -1
  116. package/dist/chunk-ZVP6GB4G.js.map +1 -0
  117. package/dist/cli.js +1 -1
  118. package/dist/cli.js.map +1 -1
  119. package/dist/client-ZLSXGPTV.js +2 -0
  120. package/dist/{compaction-NWEMOSHA.js → compaction-FD4R23HZ.js} +4 -4
  121. package/dist/config-HMW3EBZL.js +2 -0
  122. package/dist/config-mutator-4SZ5YEVD.js +2 -0
  123. package/dist/{conflict-detector-D7VIYPDI.js → conflict-detector-ZPOT3M2C.js} +2 -2
  124. package/dist/conflict-detector-ZPOT3M2C.js.map +1 -0
  125. package/dist/context-planner-AQHJMVY2.js +17 -0
  126. package/dist/context-planner-AQHJMVY2.js.map +1 -0
  127. package/dist/debug-3MEUO2P2.js +2 -0
  128. package/dist/{discover-XJXQXXIO.js → discover-D4VVRNAI.js} +2 -2
  129. package/dist/discover-D4VVRNAI.js.map +1 -0
  130. package/dist/doctor-LRYV3QLK.js +2 -0
  131. package/dist/editor-chat-OB7IQOB5.js +137 -0
  132. package/dist/editor-chat-OB7IQOB5.js.map +1 -0
  133. package/dist/entry-triage-LK34TSQD.js +2 -0
  134. package/dist/entry-triage-LK34TSQD.js.map +1 -0
  135. package/dist/facts-YVEJOM7O.js +22 -0
  136. package/dist/facts-YVEJOM7O.js.map +1 -0
  137. package/dist/heal-FVLBHYP7.js +2 -0
  138. package/dist/index.d.ts +1040 -64
  139. package/dist/index.js +12 -19
  140. package/dist/index.js.map +1 -1
  141. package/dist/{ingest-whatsapp-LZE62INA.js → ingest-whatsapp-NS4OWDOU.js} +2 -2
  142. package/dist/loader-4ZGTK7LD.js +2 -0
  143. package/dist/loader-BOKE5M5S.js +2 -0
  144. package/dist/memory-extract-F5ZHWY5E.js +2 -0
  145. package/dist/{migrate-v2-QO7SLKIM.js → migrate-v2-V4WN2VVM.js} +2 -2
  146. package/dist/ntfy-X7EPB3TG.js +4 -0
  147. package/dist/ntfy-X7EPB3TG.js.map +1 -0
  148. package/dist/processes-WU23U2OJ.js +2 -0
  149. package/dist/program-GSZ5AYAM.js +2 -0
  150. package/dist/{projects-api-KRME2YKZ.js → projects-api-QE6L65SK.js} +2 -2
  151. package/dist/{projects-mutate-3NHVG437.js → projects-mutate-MY7JTLSZ.js} +2 -2
  152. package/dist/providers-KO2DODIO.js +2 -0
  153. package/dist/query-SQDHE4HV.js +2 -0
  154. package/dist/questions-4B4KSPXG.js +3 -0
  155. package/dist/questions-4B4KSPXG.js.map +1 -0
  156. package/dist/recipes-7S4MC5OJ.js +2 -0
  157. package/dist/registry-LE6FWYZC.js +2 -0
  158. package/dist/review-candidates-RQU5KNZM.js +4 -0
  159. package/dist/review-candidates-RQU5KNZM.js.map +1 -0
  160. package/dist/review-store-B4YE3EFK.js +4 -0
  161. package/dist/review-store-B4YE3EFK.js.map +1 -0
  162. package/dist/{rotation-memo-B3EXXSEH.js → rotation-memo-7T75BLEX.js} +2 -2
  163. package/dist/routing-ILERJXB6.js +2 -0
  164. package/dist/runner-YRBVNYTY.js +2 -0
  165. package/dist/scheduler-BFM3EINS.js +2 -0
  166. package/dist/seat-7WM7II2B.js +2 -0
  167. package/dist/select-X4KVMOP5.js +2 -0
  168. package/dist/select-X4KVMOP5.js.map +1 -0
  169. package/dist/store-MCOA6N4F.js +2 -0
  170. package/dist/{subagent-WV7QKQ3L.js → subagent-2SJHSSJS.js} +2 -2
  171. package/dist/{surface-usage-PRL6Z4U3.js → surface-usage-NS74AXSE.js} +2 -2
  172. package/dist/timers-PCB4ZIBD.js +2 -0
  173. package/dist/triage-backtest-JNO7RKX7.js +2 -0
  174. package/dist/triage-backtest-JNO7RKX7.js.map +1 -0
  175. package/dist/types-H5ENIUIT.js +2 -0
  176. package/dist/types-OIFU3KSQ.js +2 -0
  177. package/dist/types-OIFU3KSQ.js.map +1 -0
  178. package/dist/web/workflow-editor.global.js +1542 -0
  179. package/dist/web/workflow-editor.global.js.map +1 -0
  180. package/dist/{whatsapp-state-XMTCSRXU.js → whatsapp-state-6D6FHHD2.js} +2 -2
  181. package/dist/wiki-2AUFSIKO.js +2 -0
  182. package/dist/wiki-2AUFSIKO.js.map +1 -0
  183. package/dist/workflows/templates/branching.yaml +1 -1
  184. package/dist/{workspace-setup-TBQAHYHJ.js → workspace-setup-B3F3TUDE.js} +2 -2
  185. package/dist/workspace-setup-B3F3TUDE.js.map +1 -0
  186. package/package.json +14 -3
  187. package/scripts/postinstall.mjs +80 -0
  188. package/dist/agent-5QMITZT6.js +0 -2
  189. package/dist/architect-SEPWMWS4.js +0 -2
  190. package/dist/board-dashboard-M3372FLO.js.map +0 -1
  191. package/dist/chunk-2WVX3LEC.js.map +0 -1
  192. package/dist/chunk-3WQFCIB6.js +0 -24
  193. package/dist/chunk-3WQFCIB6.js.map +0 -1
  194. package/dist/chunk-4YCH6IZV.js +0 -2
  195. package/dist/chunk-53NRX2NA.js +0 -2
  196. package/dist/chunk-53NRX2NA.js.map +0 -1
  197. package/dist/chunk-5U35WAZF.js.map +0 -1
  198. package/dist/chunk-BFXONUUN.js +0 -108
  199. package/dist/chunk-BFXONUUN.js.map +0 -1
  200. package/dist/chunk-BFZUG66X.js +0 -45
  201. package/dist/chunk-BFZUG66X.js.map +0 -1
  202. package/dist/chunk-BTHHSY7D.js +0 -13
  203. package/dist/chunk-BTHHSY7D.js.map +0 -1
  204. package/dist/chunk-BVGJSPW4.js.map +0 -1
  205. package/dist/chunk-D46VZ7K7.js +0 -116
  206. package/dist/chunk-D46VZ7K7.js.map +0 -1
  207. package/dist/chunk-DJEZ2HVU.js.map +0 -1
  208. package/dist/chunk-E5CXCI33.js +0 -155
  209. package/dist/chunk-E5CXCI33.js.map +0 -1
  210. package/dist/chunk-FDWOECJY.js +0 -54
  211. package/dist/chunk-FDWOECJY.js.map +0 -1
  212. package/dist/chunk-H5D7ATXS.js.map +0 -1
  213. package/dist/chunk-HR4JSRZS.js +0 -3
  214. package/dist/chunk-HR4JSRZS.js.map +0 -1
  215. package/dist/chunk-ISEBLHWX.js +0 -118
  216. package/dist/chunk-ISEBLHWX.js.map +0 -1
  217. package/dist/chunk-JP5IEXR7.js +0 -32
  218. package/dist/chunk-JTJIPNXY.js.map +0 -1
  219. package/dist/chunk-K2CAPYES.js +0 -5
  220. package/dist/chunk-LDHPMJ7M.js.map +0 -1
  221. package/dist/chunk-METYYZKQ.js +0 -1115
  222. package/dist/chunk-METYYZKQ.js.map +0 -1
  223. package/dist/chunk-MTAPOCGA.js +0 -47
  224. package/dist/chunk-MTAPOCGA.js.map +0 -1
  225. package/dist/chunk-O6YPEOQF.js +0 -5
  226. package/dist/chunk-P4VWIVKN.js.map +0 -1
  227. package/dist/chunk-SR3X25TN.js +0 -3
  228. package/dist/chunk-SR3X25TN.js.map +0 -1
  229. package/dist/chunk-UHE5FCPB.js +0 -431
  230. package/dist/chunk-UHE5FCPB.js.map +0 -1
  231. package/dist/chunk-V3U4OF7L.js.map +0 -1
  232. package/dist/chunk-VJUTQAPJ.js +0 -2
  233. package/dist/chunk-VJUTQAPJ.js.map +0 -1
  234. package/dist/chunk-VT5XYFDD.js +0 -126
  235. package/dist/chunk-VT5XYFDD.js.map +0 -1
  236. package/dist/client-HGKAC6VP.js +0 -2
  237. package/dist/config-A4RWEJHV.js +0 -2
  238. package/dist/config-mutator-MNSFUN7D.js +0 -2
  239. package/dist/conflict-detector-D7VIYPDI.js.map +0 -1
  240. package/dist/context-planner-45GEWKGL.js +0 -17
  241. package/dist/context-planner-45GEWKGL.js.map +0 -1
  242. package/dist/debug-N3B5NVJU.js +0 -2
  243. package/dist/discover-XJXQXXIO.js.map +0 -1
  244. package/dist/doctor-3Q36ZFE6.js +0 -2
  245. package/dist/heal-HZ3EQD52.js +0 -2
  246. package/dist/loader-5CA3O5GM.js +0 -2
  247. package/dist/loader-6IHUCPVO.js +0 -2
  248. package/dist/memory-extract-AENFVVO5.js +0 -2
  249. package/dist/processes-UIC4XXA6.js +0 -2
  250. package/dist/program-SAUNRCCD.js +0 -2
  251. package/dist/providers-L27BJ5A6.js +0 -2
  252. package/dist/query-HZYYRCLU.js +0 -2
  253. package/dist/recipes-EPLE2SNT.js +0 -2
  254. package/dist/registry-HK3WQ7BS.js +0 -2
  255. package/dist/runner-BFYDWPUO.js +0 -2
  256. package/dist/scheduler-6T2SU5ST.js +0 -2
  257. package/dist/store-I3HEWVLQ.js +0 -2
  258. package/dist/timers-5LPZN55J.js +0 -2
  259. package/dist/types-VECTMYTO.js +0 -2
  260. package/dist/types-Y6ZCZ52Z.js +0 -2
  261. package/dist/wiki-M6YNRFC4.js +0 -2
  262. /package/dist/{App-BPNHIGUX.js.map → App-CKPH3HHF.js.map} +0 -0
  263. /package/dist/{ChatApp-6XSSZB4U.js.map → ChatApp-MX7IG4K2.js.map} +0 -0
  264. /package/dist/{agent-5QMITZT6.js.map → agent-2U574KBZ.js.map} +0 -0
  265. /package/dist/{architect-SEPWMWS4.js.map → article-fields-L4D2BOGY.js.map} +0 -0
  266. /package/dist/{audit-JU3HWISB.js.map → audit-4245CX6P.js.map} +0 -0
  267. /package/dist/{builtin-FRKAQMFS.js.map → builtin-B67DMJ3M.js.map} +0 -0
  268. /package/dist/{chunk-4YCH6IZV.js.map → calibration-ILS5OI44.js.map} +0 -0
  269. /package/dist/{client-HGKAC6VP.js.map → chunk-FIOB3MEX.js.map} +0 -0
  270. /package/dist/{config-A4RWEJHV.js.map → client-ZLSXGPTV.js.map} +0 -0
  271. /package/dist/{compaction-NWEMOSHA.js.map → compaction-FD4R23HZ.js.map} +0 -0
  272. /package/dist/{config-mutator-MNSFUN7D.js.map → config-HMW3EBZL.js.map} +0 -0
  273. /package/dist/{debug-N3B5NVJU.js.map → config-mutator-4SZ5YEVD.js.map} +0 -0
  274. /package/dist/{doctor-3Q36ZFE6.js.map → debug-3MEUO2P2.js.map} +0 -0
  275. /package/dist/{heal-HZ3EQD52.js.map → doctor-LRYV3QLK.js.map} +0 -0
  276. /package/dist/{loader-5CA3O5GM.js.map → heal-FVLBHYP7.js.map} +0 -0
  277. /package/dist/{ingest-whatsapp-LZE62INA.js.map → ingest-whatsapp-NS4OWDOU.js.map} +0 -0
  278. /package/dist/{loader-6IHUCPVO.js.map → loader-4ZGTK7LD.js.map} +0 -0
  279. /package/dist/{memory-extract-AENFVVO5.js.map → loader-BOKE5M5S.js.map} +0 -0
  280. /package/dist/{processes-UIC4XXA6.js.map → memory-extract-F5ZHWY5E.js.map} +0 -0
  281. /package/dist/{migrate-v2-QO7SLKIM.js.map → migrate-v2-V4WN2VVM.js.map} +0 -0
  282. /package/dist/{program-SAUNRCCD.js.map → processes-WU23U2OJ.js.map} +0 -0
  283. /package/dist/{providers-L27BJ5A6.js.map → program-GSZ5AYAM.js.map} +0 -0
  284. /package/dist/{projects-api-KRME2YKZ.js.map → projects-api-QE6L65SK.js.map} +0 -0
  285. /package/dist/{projects-mutate-3NHVG437.js.map → projects-mutate-MY7JTLSZ.js.map} +0 -0
  286. /package/dist/{query-HZYYRCLU.js.map → providers-KO2DODIO.js.map} +0 -0
  287. /package/dist/{recipes-EPLE2SNT.js.map → query-SQDHE4HV.js.map} +0 -0
  288. /package/dist/{registry-HK3WQ7BS.js.map → recipes-7S4MC5OJ.js.map} +0 -0
  289. /package/dist/{runner-BFYDWPUO.js.map → registry-LE6FWYZC.js.map} +0 -0
  290. /package/dist/{rotation-memo-B3EXXSEH.js.map → rotation-memo-7T75BLEX.js.map} +0 -0
  291. /package/dist/{scheduler-6T2SU5ST.js.map → routing-ILERJXB6.js.map} +0 -0
  292. /package/dist/{store-I3HEWVLQ.js.map → runner-YRBVNYTY.js.map} +0 -0
  293. /package/dist/{surface-usage-PRL6Z4U3.js.map → scheduler-BFM3EINS.js.map} +0 -0
  294. /package/dist/{timers-5LPZN55J.js.map → seat-7WM7II2B.js.map} +0 -0
  295. /package/dist/{types-VECTMYTO.js.map → store-MCOA6N4F.js.map} +0 -0
  296. /package/dist/{subagent-WV7QKQ3L.js.map → subagent-2SJHSSJS.js.map} +0 -0
  297. /package/dist/{types-Y6ZCZ52Z.js.map → surface-usage-NS74AXSE.js.map} +0 -0
  298. /package/dist/{wiki-M6YNRFC4.js.map → timers-PCB4ZIBD.js.map} +0 -0
  299. /package/dist/{workspace-setup-TBQAHYHJ.js.map → types-H5ENIUIT.js.map} +0 -0
  300. /package/dist/{whatsapp-state-XMTCSRXU.js.map → whatsapp-state-6D6FHHD2.js.map} +0 -0
@@ -0,0 +1,185 @@
1
+ import AppKit
2
+
3
+ /// Synthetic typing and key presses.
4
+ ///
5
+ /// This is the first verb here that CHANGES something rather than pointing
6
+ /// at it, so it is worth being explicit about what that means: these
7
+ /// events are indistinguishable from the keyboard to every application,
8
+ /// they go wherever focus happens to be, and nothing downstream can tell
9
+ /// them from a person. There is no undo.
10
+ ///
11
+ /// Two consequences are built in rather than left to callers. Typing is
12
+ /// paced like a hand, so a field with its own debouncing or autocomplete
13
+ /// behaves the way it would for a person instead of receiving a paste-like
14
+ /// burst it never sees in production. And `submit` is a separate verb from
15
+ /// `type`, so committing a form is always its own deliberate call — never
16
+ /// a trailing newline that slipped into some text.
17
+ enum Typer {
18
+
19
+ /// Type text at human cadence.
20
+ ///
21
+ /// CGEventKeyboardSetUnicodeString rather than keycodes: keycodes are
22
+ /// layout-dependent, so the same call produces different characters on
23
+ /// an AZERTY machine, and this host is French.
24
+ static func type(_ text: String, wpm: Double = 260) {
25
+ // A character every ~46ms at 260wpm — brisk but visibly typed, so
26
+ // someone watching a lesson can read along.
27
+ let base = 60.0 / (wpm * 5.0)
28
+ let source = CGEventSource(stateID: .combinedSessionState)
29
+
30
+ for ch in text {
31
+ guard let down = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: true),
32
+ let up = CGEvent(keyboardEventSource: source, virtualKey: 0, keyDown: false)
33
+ else { continue }
34
+ var utf16 = Array(String(ch).utf16)
35
+ down.keyboardSetUnicodeString(stringLength: utf16.count, unicodeString: &utf16)
36
+ up.keyboardSetUnicodeString(stringLength: utf16.count, unicodeString: &utf16)
37
+ down.post(tap: .cghidEventTap)
38
+ up.post(tap: .cghidEventTap)
39
+
40
+ // Jitter, and a longer pause after a space — real typing is
41
+ // bursty at word boundaries, and a metronome is the tell.
42
+ var delay = base * Double.random(in: 0.65...1.45)
43
+ if ch == " " { delay += base * 0.9 }
44
+ Thread.sleep(forTimeInterval: delay)
45
+ }
46
+ }
47
+
48
+ /// Named keys, for the things typing cannot express.
49
+ private static let keyCodes: [String: CGKeyCode] = [
50
+ "return": 36, "enter": 36, "tab": 48, "space": 49,
51
+ "delete": 51, "escape": 53, "esc": 53,
52
+ "left": 123, "right": 124, "down": 125, "up": 126,
53
+ ]
54
+
55
+ static func press(_ name: String) -> Bool {
56
+ guard let code = keyCodes[name.lowercased()] else { return false }
57
+ let source = CGEventSource(stateID: .combinedSessionState)
58
+ CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: true)?.post(tap: .cghidEventTap)
59
+ Thread.sleep(forTimeInterval: 0.035)
60
+ CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: false)?.post(tap: .cghidEventTap)
61
+ return true
62
+ }
63
+
64
+ /// Modifier names accepted by `key` and the click verbs.
65
+ static let modifierFlags: [String: CGEventFlags] = [
66
+ "cmd": .maskCommand, "command": .maskCommand,
67
+ "shift": .maskShift,
68
+ "opt": .maskAlternate, "option": .maskAlternate, "alt": .maskAlternate,
69
+ "ctrl": .maskControl, "control": .maskControl,
70
+ "fn": .maskSecondaryFn,
71
+ ]
72
+
73
+ static func flags(from names: [String]) -> CGEventFlags {
74
+ var f = CGEventFlags()
75
+ for n in names { if let m = modifierFlags[n.lowercased()] { f.insert(m) } }
76
+ return f
77
+ }
78
+
79
+ /// Press a named key with optional modifiers, e.g. cmd+a.
80
+ static func press(_ name: String, modifiers: [String] = []) -> Bool {
81
+ guard let code = keyCodes[name.lowercased()] ?? letterCode(name) else { return false }
82
+ let source = CGEventSource(stateID: .combinedSessionState)
83
+ let mods = flags(from: modifiers)
84
+ let down = CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: true)
85
+ let up = CGEvent(keyboardEventSource: source, virtualKey: code, keyDown: false)
86
+ down?.flags = mods
87
+ up?.flags = mods
88
+ down?.post(tap: .cghidEventTap)
89
+ Thread.sleep(forTimeInterval: 0.035)
90
+ up?.post(tap: .cghidEventTap)
91
+ return true
92
+ }
93
+
94
+ /// Single letters and digits, so `key --name a --mod cmd` works.
95
+ private static func letterCode(_ name: String) -> CGKeyCode? {
96
+ let map: [Character: CGKeyCode] = [
97
+ "a": 0, "s": 1, "d": 2, "f": 3, "h": 4, "g": 5, "z": 6, "x": 7, "c": 8, "v": 9,
98
+ "b": 11, "q": 12, "w": 13, "e": 14, "r": 15, "y": 16, "t": 17,
99
+ "1": 18, "2": 19, "3": 20, "4": 21, "6": 22, "5": 23, "9": 25, "7": 26, "8": 28, "0": 29,
100
+ "o": 31, "u": 32, "i": 34, "p": 35, "l": 37, "j": 38, "k": 40, "n": 45, "m": 46,
101
+ ]
102
+ guard name.count == 1, let ch = name.lowercased().first else { return nil }
103
+ return map[ch]
104
+ }
105
+
106
+ // MARK: Mouse
107
+
108
+ /// Click where the cursor already is.
109
+ ///
110
+ /// Separate from `point` on purpose: point is safe to run against
111
+ /// anything, and keeping the click a distinct call means nothing
112
+ /// clicks merely because it was located.
113
+ static func click(button: String = "left", clicks: Int = 1, modifiers: [String] = []) {
114
+ let p = CGEvent(source: nil)?.location ?? .zero
115
+ let source = CGEventSource(stateID: .combinedSessionState)
116
+ let right = button.lowercased() == "right"
117
+ let downType: CGEventType = right ? .rightMouseDown : .leftMouseDown
118
+ let upType: CGEventType = right ? .rightMouseUp : .leftMouseUp
119
+ let mouseButton: CGMouseButton = right ? .right : .left
120
+ let mods = flags(from: modifiers)
121
+
122
+ for i in 1...max(1, clicks) {
123
+ let down = CGEvent(mouseEventSource: source, mouseType: downType,
124
+ mouseCursorPosition: p, mouseButton: mouseButton)
125
+ let up = CGEvent(mouseEventSource: source, mouseType: upType,
126
+ mouseCursorPosition: p, mouseButton: mouseButton)
127
+ // clickState is what makes the OS read two clicks as a
128
+ // double-click rather than two separate ones.
129
+ down?.setIntegerValueField(.mouseEventClickState, value: Int64(i))
130
+ up?.setIntegerValueField(.mouseEventClickState, value: Int64(i))
131
+ down?.flags = mods
132
+ up?.flags = mods
133
+ down?.post(tap: .cghidEventTap)
134
+ Thread.sleep(forTimeInterval: Double.random(in: 0.05...0.09))
135
+ up?.post(tap: .cghidEventTap)
136
+ // Inside the system double-click interval, or the second click
137
+ // starts a new sequence.
138
+ if i < clicks { Thread.sleep(forTimeInterval: 0.08) }
139
+ }
140
+ }
141
+
142
+ /// Scroll by wheel units at the cursor. Negative dy scrolls down.
143
+ static func scroll(dx: Int32 = 0, dy: Int32 = 0, steps: Int = 10) {
144
+ let source = CGEventSource(stateID: .combinedSessionState)
145
+ // Broken into steps with easing: one large wheel event jumps, and
146
+ // momentum-scrolling views ignore it entirely.
147
+ let n = max(1, steps)
148
+ for i in 1...n {
149
+ let t = Double(i) / Double(n)
150
+ let ease = 1 - pow(1 - t, 2)
151
+ let prev = Double(i - 1) / Double(n)
152
+ let prevEase = 1 - pow(1 - prev, 2)
153
+ let stepY = Int32((Double(dy) * (ease - prevEase)).rounded())
154
+ let stepX = Int32((Double(dx) * (ease - prevEase)).rounded())
155
+ if stepX == 0 && stepY == 0 { continue }
156
+ CGEvent(scrollWheelEvent2Source: source, units: .pixel,
157
+ wheelCount: 2, wheel1: stepY, wheel2: stepX, wheel3: 0)?
158
+ .post(tap: .cghidEventTap)
159
+ Thread.sleep(forTimeInterval: 0.016)
160
+ }
161
+ }
162
+
163
+ /// Press at the current point, move, release. Used for sliders,
164
+ /// reordering and selection.
165
+ static func drag(to target: CGPoint, durationSeconds: Double = 0.6) {
166
+ let from = CGEvent(source: nil)?.location ?? target
167
+ let source = CGEventSource(stateID: .combinedSessionState)
168
+ CGEvent(mouseEventSource: source, mouseType: .leftMouseDown,
169
+ mouseCursorPosition: from, mouseButton: .left)?.post(tap: .cghidEventTap)
170
+ Thread.sleep(forTimeInterval: 0.06)
171
+
172
+ let steps = max(12, Int(durationSeconds * 90))
173
+ for i in 1...steps {
174
+ let t = Double(i) / Double(steps)
175
+ let e = 10 * pow(t, 3) - 15 * pow(t, 4) + 6 * pow(t, 5)
176
+ let p = CGPoint(x: from.x + (target.x - from.x) * e,
177
+ y: from.y + (target.y - from.y) * e)
178
+ CGEvent(mouseEventSource: source, mouseType: .leftMouseDragged,
179
+ mouseCursorPosition: p, mouseButton: .left)?.post(tap: .cghidEventTap)
180
+ Thread.sleep(forTimeInterval: durationSeconds / Double(steps))
181
+ }
182
+ CGEvent(mouseEventSource: source, mouseType: .leftMouseUp,
183
+ mouseCursorPosition: target, mouseButton: .left)?.post(tap: .cghidEventTap)
184
+ }
185
+ }
@@ -0,0 +1,201 @@
1
+ import AppKit
2
+ import ApplicationServices
3
+
4
+ /// Eyes. Two of them, because "can I see it" and "did it work" are
5
+ /// different questions with different right answers.
6
+ ///
7
+ /// Everything else in this helper reads the accessibility tree, which
8
+ /// reports what EXISTS and where, and says nothing about what is VISIBLE.
9
+ /// Z-order, overlays, opacity, a modal on top, something scrolled out of
10
+ /// view — all invisible to it. An element can be reported at (218, 145)
11
+ /// with perfect confidence while something else sits on top of it.
12
+ ///
13
+ /// That is not hypothetical. This tool's own HUD covered the search field
14
+ /// it was pointing at, and nothing in the pipeline could tell. The
15
+ /// obstruction was self-inflicted and still undetectable.
16
+ ///
17
+ /// So:
18
+ ///
19
+ /// hitTest — is the thing I mean actually on top at that point?
20
+ /// Uses the accessibility hit test, which is exact and costs
21
+ /// no pixels. This is the check that catches occlusion.
22
+ ///
23
+ /// capture — what does this region look like right now? Used in pairs,
24
+ /// before and after, to answer "did anything change" — which
25
+ /// is the only honest way to claim an action landed.
26
+ enum Vision {
27
+
28
+ // MARK: Occlusion
29
+
30
+ struct HitResult: Codable {
31
+ /// Role of whatever is topmost at the point.
32
+ let role: String
33
+ let label: String
34
+ /// The frontmost app owning it — a different app here usually means
35
+ /// a floating panel is in the way.
36
+ let app: String
37
+ /// True when the topmost element's frame matches the one asked
38
+ /// about, within a tolerance.
39
+ let matchesExpected: Bool?
40
+ }
41
+
42
+ /// What is actually on top at a screen point.
43
+ ///
44
+ /// AXUIElementCopyElementAtPosition resolves through the window server,
45
+ /// so it sees exactly what a click would hit — including overlays this
46
+ /// process put there itself.
47
+ static func hitTest(x: Double, y: Double, expect: CGRect? = nil) -> HitResult? {
48
+ let system = AXUIElementCreateSystemWide()
49
+ var ref: AXUIElement?
50
+ guard AXUIElementCopyElementAtPosition(system, Float(x), Float(y), &ref) == .success,
51
+ let element = ref
52
+ else { return nil }
53
+
54
+ let role = attr(element, kAXRoleAttribute) ?? ""
55
+ let label = [kAXTitleAttribute, kAXDescriptionAttribute, kAXValueAttribute]
56
+ .compactMap { attr(element, $0) }
57
+ .first(where: { !$0.isEmpty }) ?? ""
58
+
59
+ var owner = "unknown"
60
+ var pid: pid_t = 0
61
+ if AXUIElementGetPid(element, &pid) == .success,
62
+ let app = NSRunningApplication(processIdentifier: pid) {
63
+ owner = app.localizedName ?? "unknown"
64
+ }
65
+
66
+ var matches: Bool?
67
+ if let expect {
68
+ // Compare frames rather than identity: AXUIElement references
69
+ // are not stable across calls, so two handles to the same
70
+ // control are not equal even when they should be.
71
+ if let f = frame(element) {
72
+ let dx = abs(f.midX - expect.midX), dy = abs(f.midY - expect.midY)
73
+ // Generous, because a hit inside the intended control is a
74
+ // hit even if the tree reports a child of it.
75
+ matches = dx <= max(expect.width, 24) && dy <= max(expect.height, 24)
76
+ } else {
77
+ matches = false
78
+ }
79
+ }
80
+ return HitResult(role: role, label: label, app: owner, matchesExpected: matches)
81
+ }
82
+
83
+ // MARK: Capture
84
+
85
+ /// Grab a screen region to a PNG.
86
+ ///
87
+ /// CGWindowListCreateImage rather than ScreenCaptureKit: this is a
88
+ /// one-shot CLI, and SCK's async setup costs more than the capture. It
89
+ /// needs Screen Recording permission; without it macOS returns a
90
+ /// desktop-only image rather than failing, which is why the caller is
91
+ /// told to check for an all-uniform result.
92
+ ///
93
+ /// `maxPixels` caps the TOTAL area before writing, and `maxWidth` the
94
+ /// long edge. Prefer the area budget.
95
+ ///
96
+ /// Cost tracks pixel count, not width, and capping width alone throws
97
+ /// away detail on small regions while saving nothing. A menu bar strip
98
+ /// is 2880x52 on this display — 150k pixels, a couple of hundred
99
+ /// tokens — and scaling it to 1400 wide halves the resolution of the
100
+ /// only thing in it worth seeing. A full screen is 5.2M pixels and
101
+ /// genuinely does need cutting. One rule covers both: leave it alone
102
+ /// unless it is actually big.
103
+ ///
104
+ /// Resolution here is not a cost knob, it is the difference between a
105
+ /// legible indicator and a grey smudge. macOS draws the screen-
106
+ /// recording dot at about six points.
107
+ @discardableResult
108
+ static func capture(_ rect: CGRect, to path: String,
109
+ maxWidth: Int? = nil, maxPixels: Int? = nil) -> Bool {
110
+ guard let image = CGWindowListCreateImage(
111
+ rect, .optionOnScreenOnly, kCGNullWindowID, [.bestResolution])
112
+ else { return false }
113
+ var target = maxWidth
114
+ if let budget = maxPixels, budget > 0 {
115
+ let area = image.width * image.height
116
+ if area > budget {
117
+ // Preserve aspect: width * (width / aspect) = budget.
118
+ let scale = (Double(budget) / Double(area)).squareRoot()
119
+ let fit = max(1, Int(Double(image.width) * scale))
120
+ target = min(target ?? fit, fit)
121
+ }
122
+ }
123
+ let out = target.flatMap { downscale(image, maxWidth: $0) } ?? image
124
+ let rep = NSBitmapImageRep(cgImage: out)
125
+ guard let png = rep.representation(using: .png, properties: [:]) else { return false }
126
+ return (try? png.write(to: URL(fileURLWithPath: path))) != nil
127
+ }
128
+
129
+ /// Returns nil when the image is already narrow enough, so the caller
130
+ /// falls back to the original rather than paying for a pointless redraw.
131
+ private static func downscale(_ image: CGImage, maxWidth: Int) -> CGImage? {
132
+ guard maxWidth > 0, image.width > maxWidth else { return nil }
133
+ let height = max(1, Int(Double(image.height) * Double(maxWidth) / Double(image.width)))
134
+ guard let ctx = CGContext(
135
+ data: nil, width: maxWidth, height: height,
136
+ bitsPerComponent: 8, bytesPerRow: 0,
137
+ space: CGColorSpaceCreateDeviceRGB(),
138
+ bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
139
+ else { return nil }
140
+ // High, not low: this image exists to be READ. Cheap interpolation
141
+ // turns small text into grey mush and the model then reports an
142
+ // empty menu bar with complete confidence.
143
+ ctx.interpolationQuality = .high
144
+ ctx.draw(image, in: CGRect(x: 0, y: 0, width: maxWidth, height: height))
145
+ return ctx.makeImage()
146
+ }
147
+
148
+ /// Mean absolute difference between two regions, 0…1.
149
+ ///
150
+ /// Downsamples hard before comparing: the question is "did this change
151
+ /// meaningfully", not "is a single antialiased pixel different", and a
152
+ /// small grid also makes the comparison immune to a one-pixel shift.
153
+ static func difference(_ a: CGRect, _ b: CGImage?) -> Double? {
154
+ guard let now = CGWindowListCreateImage(a, .optionOnScreenOnly, kCGNullWindowID, [.bestResolution]),
155
+ let before = b
156
+ else { return nil }
157
+ guard let g1 = grid(before), let g2 = grid(now), g1.count == g2.count, !g1.isEmpty
158
+ else { return nil }
159
+ var total = 0.0
160
+ for i in 0..<g1.count { total += abs(Double(g1[i]) - Double(g2[i])) / 255.0 }
161
+ return total / Double(g1.count)
162
+ }
163
+
164
+ static func snapshot(_ rect: CGRect) -> CGImage? {
165
+ CGWindowListCreateImage(rect, .optionOnScreenOnly, kCGNullWindowID, [.bestResolution])
166
+ }
167
+
168
+ /// 16×16 greyscale, which is plenty to notice a page changing and
169
+ /// cheap enough to run between every action.
170
+ private static func grid(_ image: CGImage, side: Int = 16) -> [UInt8]? {
171
+ let space = CGColorSpaceCreateDeviceGray()
172
+ var pixels = [UInt8](repeating: 0, count: side * side)
173
+ guard let ctx = CGContext(data: &pixels, width: side, height: side,
174
+ bitsPerComponent: 8, bytesPerRow: side,
175
+ space: space, bitmapInfo: 0)
176
+ else { return nil }
177
+ ctx.interpolationQuality = .low
178
+ ctx.draw(image, in: CGRect(x: 0, y: 0, width: side, height: side))
179
+ return pixels
180
+ }
181
+
182
+ // MARK: Helpers
183
+
184
+ private static func attr(_ node: AXUIElement, _ name: String) -> String? {
185
+ var ref: CFTypeRef?
186
+ guard AXUIElementCopyAttributeValue(node, name as CFString, &ref) == .success else { return nil }
187
+ if let s = ref as? String { return s.trimmingCharacters(in: .whitespacesAndNewlines) }
188
+ return nil
189
+ }
190
+
191
+ private static func frame(_ node: AXUIElement) -> CGRect? {
192
+ var posRef: CFTypeRef?, sizeRef: CFTypeRef?
193
+ guard AXUIElementCopyAttributeValue(node, kAXPositionAttribute as CFString, &posRef) == .success,
194
+ AXUIElementCopyAttributeValue(node, kAXSizeAttribute as CFString, &sizeRef) == .success
195
+ else { return nil }
196
+ var p = CGPoint.zero, s = CGSize.zero
197
+ AXValueGetValue(posRef as! AXValue, .cgPoint, &p)
198
+ AXValueGetValue(sizeRef as! AXValue, .cgSize, &s)
199
+ return CGRect(origin: p, size: s)
200
+ }
201
+ }
@@ -0,0 +1,265 @@
1
+ import AppKit
2
+
3
+ // agentx-mac-helper — the native half of computer use.
4
+ //
5
+ // Swift owns only what Node cannot reach: the Accessibility API and the
6
+ // window server. Everything else — which element to choose, whether the
7
+ // operation is allowed, what to do next — stays in agentx, so the
8
+ // capability belongs to every agent rather than to one app.
9
+ //
10
+ // Two verbs in this cut, and deliberately no third:
11
+ //
12
+ // read dump the focused window's accessibility tree as JSON
13
+ // point move the cursor to an element and highlight it
14
+ //
15
+ // There is no `click`. The point tier makes a model's choice visible and
16
+ // reviewable before anything irreversible happens, which is worth having
17
+ // on its own and is the only tier that is safe while the guard is still
18
+ // soaking.
19
+
20
+ func fail(_ message: String, code: Int32 = 1) -> Never {
21
+ let payload = ["ok": false, "error": message] as [String: Any]
22
+ if let d = try? JSONSerialization.data(withJSONObject: payload) {
23
+ FileHandle.standardOutput.write(d)
24
+ }
25
+ exit(code)
26
+ }
27
+
28
+ let args = Array(CommandLine.arguments.dropFirst())
29
+ guard let verb = args.first else {
30
+ fail("usage: agentx-mac-helper read | point --x N --y N --w N --h N [--label TEXT] [--hold SECONDS] [--instant] | trusted")
31
+ }
32
+
33
+ func flag(_ name: String) -> String? {
34
+ guard let i = args.firstIndex(of: "--\(name)"), i + 1 < args.count else { return nil }
35
+ return args[i + 1]
36
+ }
37
+
38
+ switch verb {
39
+ case "read":
40
+ guard AXTree.trusted() else {
41
+ fail("accessibility permission not granted — System Settings › Privacy & Security › Accessibility, then add this binary")
42
+ }
43
+ let snap = AXTree.snapshot(maxElements: Int(flag("max") ?? "400") ?? 400)
44
+ let encoder = JSONEncoder()
45
+ encoder.outputFormatting = [.withoutEscapingSlashes]
46
+ guard let data = try? encoder.encode(snap) else { fail("failed to encode tree") }
47
+ FileHandle.standardOutput.write(data)
48
+
49
+ case "point":
50
+ guard let x = Double(flag("x") ?? ""), let y = Double(flag("y") ?? ""),
51
+ let w = Double(flag("w") ?? ""), let h = Double(flag("h") ?? "") else {
52
+ fail("point needs --x --y --w --h")
53
+ }
54
+ Pointer.point(x: x, y: y, width: w, height: h,
55
+ label: flag("label") ?? "",
56
+ // Hold long enough to be seen, short enough not to be an
57
+ // obstruction. 0 is useful for scripted sequences where
58
+ // the next point follows immediately.
59
+ holdSeconds: Double(flag("hold") ?? "") ?? 2.5,
60
+ // Skip the human-speed move when something is stepping
61
+ // through many targets and the motion would just be slow.
62
+ instant: args.contains("--instant"))
63
+ FileHandle.standardOutput.write(#"{"ok":true,"pointed":true}"#.data(using: .utf8)!)
64
+
65
+ case "hittest":
66
+ // What is actually on top at this point. The occlusion check.
67
+ guard let x = Double(flag("x") ?? ""), let y = Double(flag("y") ?? "") else {
68
+ fail("hittest needs --x --y")
69
+ }
70
+ var expect: CGRect?
71
+ if let ex = Double(flag("ex") ?? ""), let ey = Double(flag("ey") ?? ""),
72
+ let ew = Double(flag("ew") ?? ""), let eh = Double(flag("eh") ?? "") {
73
+ expect = CGRect(x: ex, y: ey, width: ew, height: eh)
74
+ }
75
+ guard let hit = Vision.hitTest(x: x, y: y, expect: expect) else {
76
+ fail("nothing at (\(Int(x)), \(Int(y)))")
77
+ }
78
+ guard let hd = try? JSONEncoder().encode(hit) else { fail("failed to encode hit") }
79
+ FileHandle.standardOutput.write(hd)
80
+
81
+ case "ocr":
82
+ // Reads the focused window unless a rect is given. Coordinates come
83
+ // back in the SAME convention as `read`, so a caller can point at an
84
+ // OCR hit without converting anything.
85
+ let region: CGRect
86
+ if let x = Double(flag("x") ?? ""), let y = Double(flag("y") ?? ""),
87
+ let w = Double(flag("w") ?? ""), let h = Double(flag("h") ?? "") {
88
+ region = CGRect(x: x, y: y, width: w, height: h)
89
+ } else if let win = OCR.focusedWindowFrame() {
90
+ region = win
91
+ } else {
92
+ fail("no focused window and no --x/--y/--w/--h given")
93
+ }
94
+ let langs = (flag("lang") ?? "fr-FR,en-US").split(separator: ",").map(String.init)
95
+ let hits = OCR.read(rect: region, languages: langs)
96
+ let payload: [String: Any] = [
97
+ "ok": true,
98
+ "region": ["x": region.minX, "y": region.minY, "w": region.width, "h": region.height],
99
+ "count": hits.count,
100
+ "hits": hits.map { ["text": $0.text, "x": $0.x, "y": $0.y,
101
+ "width": $0.width, "height": $0.height,
102
+ "confidence": $0.confidence] },
103
+ ]
104
+ FileHandle.standardOutput.write(
105
+ (try? JSONSerialization.data(withJSONObject: payload)) ?? Data())
106
+
107
+ case "capture":
108
+ // Region precedence: an explicit rect, else a named region of a
109
+ // screen, else the focused window. "Whole screen" has to be reachable
110
+ // without the caller knowing the display's size, because the state
111
+ // worth checking — a recording indicator, a capture pill — lives in
112
+ // the menu bar, which belongs to no window at all.
113
+ guard let out = flag("out") else { fail("capture needs --out") }
114
+ let screen = Screens.list().first(where: { $0.index == Int(flag("screen") ?? "") })
115
+ ?? Screens.active()
116
+ let region: CGRect
117
+ if let x = Double(flag("x") ?? ""), let y = Double(flag("y") ?? ""),
118
+ let w = Double(flag("w") ?? ""), let h = Double(flag("h") ?? "") {
119
+ region = CGRect(x: x, y: y, width: w, height: h)
120
+ } else if args.contains("--menubar") {
121
+ guard let s = screen else { fail("no screen found") }
122
+ region = Screens.menuBar(s)
123
+ } else if args.contains("--screen-full") {
124
+ guard let s = screen else { fail("no screen found") }
125
+ region = Screens.rect(s)
126
+ } else if let win = OCR.focusedWindowFrame() {
127
+ region = win
128
+ } else {
129
+ fail("no focused window — pass --x/--y/--w/--h, --menubar or --screen-full")
130
+ }
131
+ guard Vision.capture(region, to: out,
132
+ maxWidth: Int(flag("max-width") ?? ""),
133
+ maxPixels: Int(flag("max-pixels") ?? "")) else {
134
+ fail("capture failed — check Screen Recording permission")
135
+ }
136
+ let shot: [String: Any] = [
137
+ "ok": true, "captured": true, "path": out,
138
+ "region": ["x": region.minX, "y": region.minY,
139
+ "w": region.width, "h": region.height],
140
+ ]
141
+ FileHandle.standardOutput.write(
142
+ (try? JSONSerialization.data(withJSONObject: shot)) ?? Data())
143
+
144
+ case "screens":
145
+ // Display geometry in top-left coordinates, so a caller can aim at a
146
+ // second monitor without redoing AppKit's origin flip.
147
+ guard let d = try? JSONEncoder().encode(Screens.list()) else { fail("failed to encode screens") }
148
+ FileHandle.standardOutput.write(d)
149
+
150
+ case "focused":
151
+ // What has keyboard focus right now. The safety check before typing.
152
+ guard let f = Focus.current() else { fail("nothing focused") }
153
+ let enc = JSONEncoder()
154
+ guard let d = try? enc.encode(f) else { fail("failed to encode focus") }
155
+ FileHandle.standardOutput.write(d)
156
+
157
+ case "type":
158
+ // Typing goes wherever focus is, and cannot tell a search box from a
159
+ // post composer. Refuses by default when focus looks publishable —
160
+ // this exists because a lesson once got one keystroke from posting a
161
+ // search query to a public timeline.
162
+ guard let text = flag("text") else { fail("type needs --text") }
163
+ if !args.contains("--force") {
164
+ guard let f = Focus.current() else {
165
+ fail("refusing to type: nothing has keyboard focus")
166
+ }
167
+ if !f.editable {
168
+ fail("refusing to type: focus is \(f.role) \"\(f.label)\", which does not accept text")
169
+ }
170
+ if f.publishRisk {
171
+ fail("refusing to type: focus looks like a compose or send field (\(f.role) \"\(f.label)\") — pass --force only if publishing is intended")
172
+ }
173
+ }
174
+ Typer.type(text, wpm: Double(flag("wpm") ?? "") ?? 260)
175
+ FileHandle.standardOutput.write(#"{"ok":true,"typed":true}"#.data(using: .utf8)!)
176
+
177
+ case "key":
178
+ guard let name = flag("name") else { fail("key needs --name (return, tab, escape, a…z, 0…9)") }
179
+ // Return is the commit key. Pressing it into a composer publishes.
180
+ if !args.contains("--force"), ["return", "enter"].contains(name.lowercased()),
181
+ let f = Focus.current(), f.publishRisk {
182
+ fail("refusing to press \(name): focus looks like a compose or send field (\(f.role) \"\(f.label)\")")
183
+ }
184
+ let mods = (flag("mod") ?? "").split(separator: "+").map(String.init)
185
+ guard Typer.press(name, modifiers: mods) else { fail("unknown key \"\(name)\"") }
186
+ FileHandle.standardOutput.write(#"{"ok":true,"pressed":true}"#.data(using: .utf8)!)
187
+
188
+ case "click":
189
+ // Clicks where the cursor already is, so a caller must point first.
190
+ // Deliberately not folded into `point`: locating something should
191
+ // never be the same act as pressing it.
192
+ //
193
+ // Before pressing, LOOK. The accessibility tree reports what exists,
194
+ // not what is visible, so a control can be reported exactly where an
195
+ // overlay is covering it — this tool's own HUD did precisely that.
196
+ // The hit test resolves through the window server and sees what a
197
+ // click would really land on.
198
+ if !args.contains("--force"), let p = CGEvent(source: nil)?.location {
199
+ if let hit = Vision.hitTest(x: p.x, y: p.y),
200
+ let expected = flag("expect"), !expected.isEmpty {
201
+ let seen = "\(hit.role) \(hit.label)".lowercased()
202
+ if !seen.contains(expected.lowercased()) {
203
+ fail("refusing to click: \(hit.app) shows \(hit.role) \"\(hit.label)\" at the cursor, not \"\(expected)\" — something is covering it")
204
+ }
205
+ }
206
+ }
207
+ // Visual first: a press people can see leads the reaction slightly,
208
+ // the way a real one does.
209
+ if !args.contains("--quiet") { Pointer.clickFlourish() }
210
+
211
+ // Snapshot a patch around the cursor so the caller can be told whether
212
+ // anything actually happened, rather than inferring it from a
213
+ // successful call.
214
+ var beforeShot: CGImage?
215
+ var patch = CGRect.zero
216
+ if let p = CGEvent(source: nil)?.location {
217
+ patch = CGRect(x: p.x - 90, y: p.y - 60, width: 180, height: 120)
218
+ beforeShot = Vision.snapshot(patch)
219
+ }
220
+
221
+ Typer.click(button: flag("button") ?? "left",
222
+ clicks: Int(flag("clicks") ?? "1") ?? 1,
223
+ modifiers: (flag("mod") ?? "").split(separator: "+").map(String.init))
224
+
225
+ // A UI needs a frame or two to react.
226
+ Thread.sleep(forTimeInterval: Double(flag("settle") ?? "") ?? 0.45)
227
+ var changed: Bool?
228
+ if beforeShot != nil, let diff = Vision.difference(patch, beforeShot) {
229
+ // 1.5% mean absolute difference over a 16x16 grid: enough to catch
230
+ // a button state or a menu opening, high enough to ignore a
231
+ // blinking caret.
232
+ changed = diff > 0.015
233
+ }
234
+ let payload: [String: Any] = [
235
+ "ok": true, "clicked": true,
236
+ "changed": changed as Any,
237
+ ]
238
+ FileHandle.standardOutput.write(
239
+ (try? JSONSerialization.data(withJSONObject: payload)) ?? Data())
240
+
241
+ case "scroll":
242
+ Typer.scroll(dx: Int32(flag("dx") ?? "0") ?? 0,
243
+ dy: Int32(flag("dy") ?? "0") ?? 0,
244
+ steps: Int(flag("steps") ?? "10") ?? 10)
245
+ FileHandle.standardOutput.write(#"{"ok":true,"scrolled":true}"#.data(using: .utf8)!)
246
+
247
+ case "drag":
248
+ guard let tx = Double(flag("to-x") ?? ""), let ty = Double(flag("to-y") ?? "") else {
249
+ fail("drag needs --to-x --to-y (press starts wherever the cursor is)")
250
+ }
251
+ Typer.drag(to: CGPoint(x: tx, y: ty),
252
+ durationSeconds: Double(flag("duration") ?? "") ?? 0.6)
253
+ FileHandle.standardOutput.write(#"{"ok":true,"dragged":true}"#.data(using: .utf8)!)
254
+
255
+ case "hud":
256
+ // Long-lived: reads update lines on stdin until EOF.
257
+ HUD.run()
258
+
259
+ case "trusted":
260
+ let payload = ["ok": true, "trusted": AXTree.trusted()] as [String: Any]
261
+ FileHandle.standardOutput.write(try! JSONSerialization.data(withJSONObject: payload))
262
+
263
+ default:
264
+ fail("unknown verb \"\(verb)\" — expected read, screens, point, click, type, key, scroll, drag, ocr, capture, hittest, focused, hud or trusted")
265
+ }