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,39 @@
1
+ #!/bin/bash
2
+ # Build the native computer-use helper.
3
+ #
4
+ # Bundled as a .app even though it is a CLI: macOS only offers an
5
+ # Accessibility permission prompt to a bundled, signed binary, and a bare
6
+ # executable silently reads an empty tree instead — which looks exactly
7
+ # like an app with no controls.
8
+ set -euo pipefail
9
+ cd "$(dirname "$0")"
10
+
11
+ APP="build/AgentX Helper.app"
12
+ BIN="$APP/Contents/MacOS"
13
+ rm -rf build && mkdir -p "$BIN"
14
+
15
+ swiftc -O -o "$BIN/agentx-mac-helper" \
16
+ Sources/AgentXHelper/*.swift \
17
+ -framework AppKit -framework ApplicationServices -framework Vision \
18
+ -target arm64-apple-macosx14.0
19
+
20
+ cat > "$APP/Contents/Info.plist" <<'PLIST'
21
+ <?xml version="1.0" encoding="UTF-8"?>
22
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
23
+ <plist version="1.0">
24
+ <dict>
25
+ <key>CFBundleName</key><string>AgentX Helper</string>
26
+ <key>CFBundleIdentifier</key><string>tn.acme.agentx.helper</string>
27
+ <key>CFBundleExecutable</key><string>agentx-mac-helper</string>
28
+ <key>CFBundlePackageType</key><string>APPL</string>
29
+ <key>CFBundleShortVersionString</key><string>0.1.0</string>
30
+ <key>LSUIElement</key><true/>
31
+ </dict>
32
+ PLIST
33
+ echo "</plist>" >> "$APP/Contents/Info.plist"
34
+
35
+ codesign --force --sign - --identifier tn.acme.agentx.helper "$APP" 2>/dev/null \
36
+ || echo "warning: codesign failed; Accessibility grants will not stick across rebuilds"
37
+
38
+ echo "built: $APP"
39
+ echo "binary: $BIN/agentx-mac-helper"
@@ -0,0 +1,18 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleName</key><string>AgentX Desktop</string>
6
+ <key>CFBundleDisplayName</key><string>AgentX Desktop</string>
7
+ <key>CFBundleIdentifier</key><string>tn.acme.agentx.voice</string>
8
+ <key>CFBundleExecutable</key><string>AgentXVoice</string>
9
+ <key>CFBundlePackageType</key><string>APPL</string>
10
+ <key>CFBundleShortVersionString</key><string>0.1.0</string>
11
+ <key>CFBundleVersion</key><string>1</string>
12
+ <key>LSMinimumSystemVersion</key><string>14.0</string>
13
+ <!-- Accessory: floats over other apps, no Dock icon, never steals focus. -->
14
+ <key>LSUIElement</key><true/>
15
+ <key>NSMicrophoneUsageDescription</key>
16
+ <string>AgentX Desktop records your voice only while you hold the hotkey, so it can transcribe your instruction and send it to your agent.</string>
17
+ </dict>
18
+ </plist>
@@ -0,0 +1,108 @@
1
+ import Foundation
2
+
3
+ /// Talks to the local agentx daemon's /ask endpoint.
4
+ ///
5
+ /// /ask already does the voice-specific work server-side: it prepends a
6
+ /// VOICE MODE instruction and returns `text` pre-flattened for TTS by
7
+ /// toSpeakable(). So this client deliberately does no prompt shaping of
8
+ /// its own — two places deciding how an agent should sound is how they
9
+ /// drift apart.
10
+ ///
11
+ /// No auth header: the daemon exempts loopback (see mesh-auth.ts), and
12
+ /// this widget is loopback by definition. Pointing AGENTX_DAEMON_URL at a
13
+ /// remote node would need a token, which is why it isn't the default.
14
+ enum AgentClient {
15
+ struct Answer {
16
+ /// What to speak: short, no URLs, no markdown.
17
+ let text: String
18
+ /// What to show: the answer as written, with the links and
19
+ /// formatting the spoken form had to drop.
20
+ let written: String?
21
+ /// Buttons and media from the agentx:ui directive, parsed server-side.
22
+ let buttons: [(String, String)]
23
+ let imageURL: String?
24
+ let durationMs: Int?
25
+ }
26
+
27
+ /// What, if anything, to say about the step now running.
28
+ ///
29
+ /// The daemon decides, because the phrasing lives with the model that
30
+ /// chooses it. Returning nil is the common and correct case — most
31
+ /// steps are not worth interrupting for, and the pill already shows
32
+ /// every one of them to anyone looking.
33
+ ///
34
+ /// Never throws: a narration that fails is silence, which is exactly
35
+ /// what it would have been anyway.
36
+ static func phrase(tool: String, detail: String, elapsed: Int) async -> String? {
37
+ guard let url = URL(string: "\(Config.daemonURL)/voice/phrase") else { return nil }
38
+ var req = URLRequest(url: url)
39
+ req.httpMethod = "POST"
40
+ // Shorter than the turn it narrates: a phrase that arrives late
41
+ // describes work already finished.
42
+ req.timeoutInterval = 6
43
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
44
+ req.httpBody = try? JSONSerialization.data(withJSONObject: [
45
+ "tool": tool, "detail": detail, "elapsedSeconds": elapsed,
46
+ ])
47
+ guard let (data, _) = try? await URLSession.shared.data(for: req) else { return nil }
48
+ struct Reply: Decodable { let say: String? }
49
+ return (try? JSONDecoder().decode(Reply.self, from: data))?.say
50
+ }
51
+
52
+ static func ask(_ message: String) async throws -> Answer {
53
+ var req = URLRequest(url: URL(string: "\(Config.daemonURL)/ask")!)
54
+ req.httpMethod = "POST"
55
+ // An agent turn routinely takes minutes. The default 60s would cut
56
+ // off exactly the thoughtful answers worth waiting for.
57
+ req.timeoutInterval = 600
58
+ req.setValue("application/json", forHTTPHeaderField: "Content-Type")
59
+ req.httpBody = try JSONSerialization.data(withJSONObject: [
60
+ "message": message,
61
+ "agent": Config.agentID,
62
+ ])
63
+
64
+ let (data, response) = try await URLSession.shared.data(for: req)
65
+ struct UiButton: Decodable { let label: String; let url: String }
66
+ struct UiMedia: Decodable { let type: String; let url: String; let caption: String? }
67
+ struct Ui: Decodable { let buttons: [UiButton]?; let media: UiMedia? }
68
+ struct Reply: Decodable {
69
+ let text: String?
70
+ let full: String?
71
+ let ui: Ui?
72
+ let error: String?
73
+ let duration: Int?
74
+ }
75
+ let reply = try? JSONDecoder().decode(Reply.self, from: data)
76
+
77
+ // 202 means accepted-but-busy: the daemon has already written a
78
+ // speakable explanation into `text`. Falling through to the error
79
+ // path here is what made "she is still working" sound like "that
80
+ // didn't work".
81
+ if let http = response as? HTTPURLResponse, http.statusCode == 202,
82
+ let queuedText = reply?.text, !queuedText.isEmpty {
83
+ return Answer(text: queuedText, written: nil, buttons: [], imageURL: nil,
84
+ durationMs: reply?.duration)
85
+ }
86
+
87
+ if let http = response as? HTTPURLResponse, !(200..<300).contains(http.statusCode) {
88
+ let detail = reply?.error ?? String(data: data.prefix(200), encoding: .utf8) ?? ""
89
+ throw VoiceError.api("daemon HTTP \(http.statusCode): \(detail)")
90
+ }
91
+ if let err = reply?.error, !err.isEmpty { throw VoiceError.api(err) }
92
+ guard let text = reply?.text ?? reply?.full, !text.isEmpty else {
93
+ throw VoiceError.api("agent returned an empty answer")
94
+ }
95
+ let buttons = (reply?.ui?.buttons ?? []).map { ($0.label, $0.url) }
96
+ // Only images are shown inline; a document or video is offered as a
97
+ // link instead, because a card that cannot play it should not
98
+ // pretend otherwise.
99
+ let media = reply?.ui?.media
100
+ let image = media?.type == "image" ? media?.url : nil
101
+ let extra: [(String, String)] = (media != nil && image == nil)
102
+ ? [(media!.caption ?? media!.type.capitalized, media!.url)] : []
103
+
104
+ return Answer(text: text, written: reply?.full,
105
+ buttons: buttons + extra, imageURL: image,
106
+ durationMs: reply?.duration)
107
+ }
108
+ }
@@ -0,0 +1,123 @@
1
+ import AppKit
2
+
3
+ /// Acme design tokens, transcribed for AppKit.
4
+ ///
5
+ /// Copied from the design system's `tokens/` rather than eyeballed, so the
6
+ /// widget belongs to the same product as example.com instead of merely
7
+ /// looking tidy. Two rules from that system shape most of what follows:
8
+ ///
9
+ /// - Mono is UPPERCASE with 0.06–0.08em tracking, and is reserved for
10
+ /// eyebrows, status and meta. Body stays sentence case.
11
+ /// - No emoji, ever. Texture comes from Unicode punctuation — · → ● ✓ —
12
+ /// and from a single accent colour, not from decoration.
13
+ ///
14
+ /// System colours were the obvious default and the wrong one: .systemRed
15
+ /// and .systemGreen are macOS's voice, not Acme's, and they shift under
16
+ /// the user's accent-colour setting.
17
+ enum Brand {
18
+
19
+ // Teal accent and deep blue primary — the two colours the whole
20
+ // identity rests on.
21
+ static let accent = NSColor(srgbRed: 0.078, green: 0.722, blue: 0.651, alpha: 1) // #14b8a6
22
+ static let accentDeep = NSColor(srgbRed: 0.059, green: 0.463, blue: 0.431, alpha: 1) // #0f766e
23
+ static let primary = NSColor(srgbRed: 0.118, green: 0.227, blue: 0.541, alpha: 1) // #1e3a8a
24
+ static let primaryBright = NSColor(srgbRed: 0.114, green: 0.306, blue: 0.847, alpha: 1) // #1d4ed8
25
+
26
+ static let ink = NSColor(srgbRed: 0.043, green: 0.078, blue: 0.075, alpha: 1) // #0b1413
27
+ static let paper = NSColor(srgbRed: 0.980, green: 0.980, blue: 0.976, alpha: 1) // #fafaf9
28
+
29
+ /// Amber and rose for states the palette has no token for. Muted to
30
+ /// sit beside the teal rather than fight it.
31
+ static let warn = NSColor(srgbRed: 0.851, green: 0.596, blue: 0.180, alpha: 1)
32
+ static let alert = NSColor(srgbRed: 0.804, green: 0.400, blue: 0.376, alpha: 1)
33
+
34
+ enum Radius {
35
+ static let sm: CGFloat = 6
36
+ static let base: CGFloat = 10
37
+ static let md: CGFloat = 12
38
+ static let lg: CGFloat = 18
39
+ static let pill: CGFloat = 999
40
+ }
41
+
42
+ /// `--nq-ease` — the single easing curve the system uses everywhere.
43
+ static let ease = CAMediaTimingFunction(controlPoints: 0.65, 0, 0.35, 1)
44
+ static let durFast: CFTimeInterval = 0.15
45
+
46
+ /// Meta type: mono, uppercase, tracked. Eyebrows and status only.
47
+ static func meta(size: CGFloat = 10.5) -> [NSAttributedString.Key: Any] {
48
+ [
49
+ .font: NSFont.monospacedSystemFont(ofSize: size, weight: .semibold),
50
+ .kern: size * 0.07,
51
+ ]
52
+ }
53
+
54
+ static func metaString(_ text: String, size: CGFloat = 10.5, color: NSColor) -> NSAttributedString {
55
+ var attrs = meta(size: size)
56
+ attrs[.foregroundColor] = color
57
+ return NSAttributedString(string: text.uppercased(), attributes: attrs)
58
+ }
59
+
60
+ static func body(size: CGFloat = 13, weight: NSFont.Weight = .medium) -> NSFont {
61
+ NSFont.systemFont(ofSize: size, weight: weight)
62
+ }
63
+
64
+ /// The CSS the result card renders markdown into. Same tokens, so an
65
+ /// answer reads like the product it came from.
66
+ static let cardCSS = """
67
+ :root {
68
+ color-scheme: light dark;
69
+ --nq-accent: #14b8a6;
70
+ --nq-primary: #1e3a8a;
71
+ --nq-ink: #0b1413;
72
+ --nq-paper: #fafaf9;
73
+ --nq-radius-sm: 6px;
74
+ --nq-radius: 10px;
75
+ }
76
+ @media (prefers-color-scheme: dark) {
77
+ :root { --nq-ink: #f4f4f2; --nq-primary: #93c5fd; }
78
+ }
79
+ body {
80
+ font: 13px/1.55 -apple-system, 'Geist', system-ui, sans-serif;
81
+ margin: 14px 14px 10px; color: canvastext; background: transparent;
82
+ word-wrap: break-word; -webkit-font-smoothing: antialiased;
83
+ }
84
+ h1, h2, h3, h4 {
85
+ margin: .75em 0 .3em; font-size: 1.02em; font-weight: 650;
86
+ letter-spacing: -0.005em;
87
+ }
88
+ h1:first-child, h2:first-child, h3:first-child { margin-top: 0; }
89
+ p { margin: .45em 0; }
90
+ ul { margin: .4em 0; padding-left: 1.15em; }
91
+ li { margin: .2em 0; }
92
+ li::marker { color: var(--nq-accent); }
93
+ strong { font-weight: 640; }
94
+ code {
95
+ font: 11.5px ui-monospace, 'Geist Mono', SFMono-Regular, monospace;
96
+ background: color-mix(in srgb, var(--nq-accent) 12%, transparent);
97
+ padding: 1.5px 5px; border-radius: var(--nq-radius-sm);
98
+ }
99
+ pre {
100
+ background: color-mix(in srgb, canvastext 7%, transparent);
101
+ padding: 10px 12px; border-radius: var(--nq-radius);
102
+ overflow-x: auto; line-height: 1.45;
103
+ border-left: 2px solid color-mix(in srgb, var(--nq-accent) 55%, transparent);
104
+ }
105
+ pre code { background: none; padding: 0; }
106
+ blockquote {
107
+ margin: .5em 0; padding-left: .85em;
108
+ border-left: 2px solid color-mix(in srgb, var(--nq-accent) 45%, transparent);
109
+ opacity: .85;
110
+ }
111
+ a { color: var(--nq-primary); text-underline-offset: 2px; }
112
+ hr {
113
+ border: none; margin: 1em 0;
114
+ border-top: 1px solid color-mix(in srgb, canvastext 14%, transparent);
115
+ }
116
+ table { border-collapse: collapse; margin: .5em 0; font-size: 12px; }
117
+ th, td { padding: 4px 9px; border: 1px solid color-mix(in srgb, canvastext 14%, transparent); }
118
+ th {
119
+ font: 10px ui-monospace, monospace; text-transform: uppercase;
120
+ letter-spacing: .07em; text-align: left; font-weight: 600;
121
+ }
122
+ """
123
+ }
@@ -0,0 +1,68 @@
1
+ import Foundation
2
+
3
+ /// Everything the widget needs to find, resolved once at launch.
4
+ ///
5
+ /// Keys are read from files first and the environment second, because a
6
+ /// GUI app launched from Finder inherits almost nothing from a login
7
+ /// shell — an app that only reads the environment works when you run it
8
+ /// from a terminal and mysteriously fails when you double-click it.
9
+ enum Config {
10
+ static let daemonURL = env("AGENTX_DAEMON_URL") ?? "http://127.0.0.1:18800"
11
+ static let agentID = env("AGENTX_VOICE_AGENT") ?? "secretary-agent"
12
+
13
+ /// ElevenLabs default voice. Override with AGENTX_VOICE_ID.
14
+ static let voiceID = env("AGENTX_VOICE_ID") ?? "21m00Tcm4TlvDq8ikWAM"
15
+ static let sttModel = env("AGENTX_STT_MODEL") ?? "scribe_v1"
16
+
17
+ /// How to invoke smart paste. Run through a login shell, so this is a
18
+ /// command line rather than a path.
19
+ ///
20
+ /// Environment, then a file, then a guess — the same order as the keys
21
+ /// below and for the same reason, with one extra: `agentx` on PATH is
22
+ /// very often the WRONG agentx. It is commonly a globally installed
23
+ /// copy of the published package rather than the local build, so it
24
+ /// silently lacks anything unreleased, and a login shell may resolve
25
+ /// `node` to a version old enough that the CLI will not start at all.
26
+ /// Both were true on the machine this was written on. The file is how
27
+ /// a machine says which build it actually means.
28
+ /// How to invoke `agentx notify`. Derived from pasteCommand so a
29
+ /// machine only has to say once where its agentx lives.
30
+ static var notifyCommand: String {
31
+ pasteCommand.replacingOccurrences(of: " paste", with: " notify")
32
+ }
33
+
34
+ static let pasteCommand: String = {
35
+ if let c = env("AGENTX_PASTE_COMMAND"), !c.isEmpty { return c }
36
+ if let s = try? String(contentsOfFile: "\(NSHomeDirectory())/.agentx/paste-command.txt",
37
+ encoding: .utf8) {
38
+ let t = s.trimmingCharacters(in: .whitespacesAndNewlines)
39
+ if !t.isEmpty { return t }
40
+ }
41
+ return "agentx paste"
42
+ }()
43
+ static let ttsModel = env("AGENTX_TTS_MODEL") ?? "eleven_turbo_v2_5"
44
+
45
+ static let mlxWhisper = env("AGENTX_MLX_WHISPER")
46
+ ?? "\(NSHomeDirectory())/.local/bin/mlx_whisper"
47
+ static let mlxModel = env("AGENTX_MLX_MODEL") ?? "mlx-community/whisper-large-v3-turbo"
48
+
49
+ /// nil when no key is available anywhere — the app still runs, falling
50
+ /// back to local STT and `say`, which is the whole point of having a
51
+ /// fallback rather than refusing to start.
52
+ static let elevenLabsKey: String? = {
53
+ if let k = env("ELEVENLABS_API_KEY"), !k.isEmpty { return k }
54
+ for path in ["\(NSHomeDirectory())/.elevenlabs/key",
55
+ "\(NSHomeDirectory())/.agentx/elevenlabs-key.txt"] {
56
+ if let s = try? String(contentsOfFile: path, encoding: .utf8) {
57
+ let t = s.trimmingCharacters(in: .whitespacesAndNewlines)
58
+ if !t.isEmpty { return t }
59
+ }
60
+ }
61
+ return nil
62
+ }()
63
+
64
+ private static func env(_ k: String) -> String? {
65
+ guard let v = ProcessInfo.processInfo.environment[k], !v.isEmpty else { return nil }
66
+ return v
67
+ }
68
+ }
@@ -0,0 +1,74 @@
1
+ import Foundation
2
+
3
+ /// "Don't interrupt me" — the widget's own switch.
4
+ ///
5
+ /// Separate from macOS Focus on purpose. System Focus is a heavy
6
+ /// instrument: it silences every device you own and announces itself to
7
+ /// everyone who messages you, which is far more than someone means when
8
+ /// they only want to finish a thought. A switch on the widget is a hold
9
+ /// you flick on for twenty minutes and forget.
10
+ ///
11
+ /// The state lives in a file rather than in this process because the
12
+ /// thing that HONOURS it is not this process — `agentx notify` runs in the
13
+ /// daemon and in agents' shells, and none of them can ask a widget a
14
+ /// question. A file is the smallest thing all of them can read.
15
+ ///
16
+ /// Either source counts as held: agentx checks this file and the system's
17
+ /// Focus database, and neither overrides the other. They are the same
18
+ /// person saying the same thing by different means.
19
+ enum Hold {
20
+
21
+ private static let path: URL = {
22
+ let dir = FileManager.default.homeDirectoryForCurrentUser
23
+ .appendingPathComponent(".agentx", isDirectory: true)
24
+ return dir.appendingPathComponent("focus.json")
25
+ }()
26
+
27
+ /// Is the widget currently holding notifications?
28
+ static var isOn: Bool {
29
+ guard let data = try? Data(contentsOf: path),
30
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
31
+ else { return false }
32
+ return (obj["active"] as? Bool) ?? false
33
+ }
34
+
35
+ /// Turn the hold on or off. Returns the new state.
36
+ @discardableResult
37
+ static func set(_ on: Bool) -> Bool {
38
+ let payload: [String: Any] = [
39
+ "active": on,
40
+ "since": ISO8601DateFormatter().string(from: Date()),
41
+ // Recorded so an operator reading the file knows which switch
42
+ // produced it — the widget's, not a stale test fixture.
43
+ "source": "widget",
44
+ ]
45
+ do {
46
+ try FileManager.default.createDirectory(
47
+ at: path.deletingLastPathComponent(), withIntermediateDirectories: true)
48
+ let data = try JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted])
49
+ try data.write(to: path, options: .atomic)
50
+ } catch {
51
+ Log.warn("could not write the hold state: \(error.localizedDescription)")
52
+ }
53
+ return on
54
+ }
55
+
56
+ @discardableResult
57
+ static func toggle() -> Bool { set(!isOn) }
58
+
59
+ /// Ask agentx to deliver anything that piled up while the hold was on.
60
+ ///
61
+ /// Fire-and-forget through the same command line the widget uses for
62
+ /// smart paste, so there is one place that knows how to find the CLI.
63
+ static func flushHeld() {
64
+ DispatchQueue.global(qos: .utility).async {
65
+ let process = Process()
66
+ process.executableURL = URL(fileURLWithPath: "/bin/zsh")
67
+ process.arguments = ["-lc", Config.notifyCommand + " --flush"]
68
+ process.standardOutput = Pipe()
69
+ process.standardError = Pipe()
70
+ try? process.run()
71
+ process.waitUntilExit()
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,76 @@
1
+ import AppKit
2
+ import Carbon.HIToolbox
3
+
4
+ /// Global push-to-talk hotkey, default ⌥Space.
5
+ ///
6
+ /// Carbon's RegisterEventHotKey rather than an NSEvent global monitor,
7
+ /// because the monitor route requires Accessibility permission and this
8
+ /// slice should need nothing but the microphone. Carbon also reports key
9
+ /// RELEASE, which is what makes hold-to-talk possible at all.
10
+ ///
11
+ /// ⌥Space and not ⌃Space: control-space is bound to input-source switching
12
+ /// on many Macs, and silently stealing it is a bad first impression.
13
+ ///
14
+ /// Each instance carries its own `id` and IGNORES events for any other.
15
+ /// That is not decoration: every installed handler is called for EVERY
16
+ /// registered hotkey, so a second binding without this filter makes both
17
+ /// actions fire on either key.
18
+ final class Hotkey {
19
+ private var ref: EventHotKeyRef?
20
+ private var handler: EventHandlerRef?
21
+ private let onPress: () -> Void
22
+ private let onRelease: () -> Void
23
+ private let id: UInt32
24
+
25
+ init(id: UInt32 = 1, onPress: @escaping () -> Void, onRelease: @escaping () -> Void) {
26
+ self.id = id
27
+ self.onPress = onPress
28
+ self.onRelease = onRelease
29
+ }
30
+
31
+ func register(keyCode: UInt32 = UInt32(kVK_Space), modifiers: UInt32 = UInt32(optionKey)) {
32
+ var spec = [
33
+ EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed)),
34
+ EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyReleased)),
35
+ ]
36
+ let context = Unmanaged.passUnretained(self).toOpaque()
37
+ InstallEventHandler(GetApplicationEventTarget(), { _, event, ctx in
38
+ guard let ctx, let event else { return noErr }
39
+ let me = Unmanaged<Hotkey>.fromOpaque(ctx).takeUnretainedValue()
40
+
41
+ // Whose key was it? Handlers are global, so this instance must
42
+ // discard anything that is not its own.
43
+ //
44
+ // Filter ONLY on a successful lookup. Ignoring the status here
45
+ // left `fired` zero-initialised whenever the call failed, so
46
+ // the guard rejected every event and BOTH hotkeys went dead —
47
+ // silently, because a hotkey that does nothing looks exactly
48
+ // like a hotkey that was never pressed.
49
+ //
50
+ // Falling through on failure is the right degradation: the
51
+ // worst case is the older bug where both bindings react to
52
+ // either key, which is visible and recoverable. Silence is
53
+ // neither.
54
+ var fired = EventHotKeyID()
55
+ let status = GetEventParameter(
56
+ event, EventParamName(kEventParamDirectObject),
57
+ EventParamType(typeEventHotKeyID), nil,
58
+ MemoryLayout<EventHotKeyID>.size, nil, &fired)
59
+ if status == noErr, fired.id != me.id { return noErr }
60
+
61
+ let kind = GetEventKind(event)
62
+ DispatchQueue.main.async {
63
+ if kind == UInt32(kEventHotKeyPressed) { me.onPress() } else { me.onRelease() }
64
+ }
65
+ return noErr
66
+ }, spec.count, &spec, context, &handler)
67
+
68
+ let hotKeyID = EventHotKeyID(signature: OSType(0x41475856), id: id) // 'AGXV'
69
+ RegisterEventHotKey(keyCode, modifiers, hotKeyID, GetApplicationEventTarget(), 0, &ref)
70
+ }
71
+
72
+ deinit {
73
+ if let ref { UnregisterEventHotKey(ref) }
74
+ if let handler { RemoveEventHandler(handler) }
75
+ }
76
+ }
@@ -0,0 +1,140 @@
1
+ import Foundation
2
+
3
+ /// Markdown to HTML, small and dependency-free.
4
+ ///
5
+ /// Agents write markdown by default — headings, bullets, fenced code,
6
+ /// tables. Rendered as plain text it is a mess of asterisks and hashes,
7
+ /// which is worse than useless when the whole point of the card is that
8
+ /// the written answer is more readable than the spoken one.
9
+ ///
10
+ /// A real parser would be better and is not worth a dependency here. This
11
+ /// handles what agents actually produce; anything it does not recognise
12
+ /// falls through as text rather than breaking the page.
13
+ enum Markdown {
14
+
15
+ /// Fallback styling when no design tokens are supplied.
16
+ static let defaultCSS = """
17
+ body { font: -apple-system-body; font-size: 13px; margin: 12px; line-height: 1.5;
18
+ color: canvastext; background: transparent; word-wrap: break-word; }
19
+ code { font: ui-monospace, monospace; font-size: 12px;
20
+ background: color-mix(in srgb, canvastext 10%, transparent);
21
+ padding: 1px 4px; border-radius: 4px; }
22
+ pre { background: color-mix(in srgb, canvastext 8%, transparent);
23
+ padding: 8px 10px; border-radius: 6px; overflow-x: auto; }
24
+ a { color: linktext; }
25
+ """
26
+
27
+
28
+ static func toHTML(_ markdown: String) -> String {
29
+ var html = ""
30
+ var inCode = false
31
+ var listDepth = 0
32
+
33
+ func closeList() {
34
+ while listDepth > 0 { html += "</ul>"; listDepth -= 1 }
35
+ }
36
+
37
+ for rawLine in markdown.components(separatedBy: .newlines) {
38
+ let line = rawLine
39
+
40
+ // Fenced code: everything inside is literal, including things
41
+ // that would otherwise look like markdown.
42
+ if line.trimmingCharacters(in: .whitespaces).hasPrefix("```") {
43
+ if inCode { html += "</code></pre>" } else { closeList(); html += "<pre><code>" }
44
+ inCode.toggle()
45
+ continue
46
+ }
47
+ if inCode { html += escape(line) + "\n"; continue }
48
+
49
+ let trimmed = line.trimmingCharacters(in: .whitespaces)
50
+ if trimmed.isEmpty { closeList(); html += "<p></p>"; continue }
51
+
52
+ // Headings
53
+ if let h = heading(trimmed) {
54
+ closeList()
55
+ html += "<h\(h.level)>\(inline(h.text))</h\(h.level)>"
56
+ continue
57
+ }
58
+
59
+ // Bullets and numbered items, both rendered as a list — the
60
+ // marker style matters far less than the indentation reading
61
+ // correctly.
62
+ if let item = listItem(trimmed) {
63
+ if listDepth == 0 { html += "<ul>"; listDepth = 1 }
64
+ html += "<li>\(inline(item))</li>"
65
+ continue
66
+ }
67
+
68
+ if trimmed.hasPrefix("> ") {
69
+ closeList()
70
+ html += "<blockquote>\(inline(String(trimmed.dropFirst(2))))</blockquote>"
71
+ continue
72
+ }
73
+
74
+ // Horizontal rule
75
+ if trimmed == "---" || trimmed == "***" { closeList(); html += "<hr>"; continue }
76
+
77
+ closeList()
78
+ html += "<p>\(inline(trimmed))</p>"
79
+ }
80
+ closeList()
81
+ if inCode { html += "</code></pre>" }
82
+ return html
83
+ }
84
+
85
+ private static func heading(_ line: String) -> (level: Int, text: String)? {
86
+ var level = 0
87
+ var rest = Substring(line)
88
+ while rest.first == "#" && level < 6 { level += 1; rest = rest.dropFirst() }
89
+ guard level > 0, rest.first == " " else { return nil }
90
+ return (level, String(rest.dropFirst()))
91
+ }
92
+
93
+ private static func listItem(_ line: String) -> String? {
94
+ for marker in ["- ", "* ", "+ "] {
95
+ if line.hasPrefix(marker) { return String(line.dropFirst(marker.count)) }
96
+ }
97
+ // "1. item"
98
+ if let dot = line.firstIndex(of: "."), line[line.startIndex..<dot].allSatisfy(\.isNumber),
99
+ line.index(after: dot) < line.endIndex, line[line.index(after: dot)] == " " {
100
+ return String(line[line.index(dot, offsetBy: 2)...])
101
+ }
102
+ return nil
103
+ }
104
+
105
+ /// Inline spans. Order matters: code first, so its contents are not
106
+ /// then treated as emphasis.
107
+ private static func inline(_ text: String) -> String {
108
+ var s = escape(text)
109
+ s = replace(s, #"`([^`]+)`"#, "<code>$1</code>")
110
+ s = replace(s, #"\*\*([^*]+)\*\*"#, "<strong>$1</strong>")
111
+ s = replace(s, #"(?<![\*\w])\*([^*]+)\*(?![\*\w])"#, "<em>$1</em>")
112
+ s = replace(s, #"\[([^\]]+)\]\(([^)\s]+)\)"#, "<a href=\"$2\">$1</a>")
113
+ // Bare URLs, but not ones already inside an href.
114
+ s = replace(s, #"(?<!["=])\b(https?://[^\s<)]+)"#, "<a href=\"$1\">$1</a>")
115
+ return s
116
+ }
117
+
118
+ private static func replace(_ s: String, _ pattern: String, _ template: String) -> String {
119
+ guard let re = try? NSRegularExpression(pattern: pattern) else { return s }
120
+ return re.stringByReplacingMatches(
121
+ in: s, range: NSRange(s.startIndex..., in: s), withTemplate: template)
122
+ }
123
+
124
+ private static func escape(_ s: String) -> String {
125
+ s.replacingOccurrences(of: "&", with: "&amp;")
126
+ .replacingOccurrences(of: "<", with: "&lt;")
127
+ .replacingOccurrences(of: ">", with: "&gt;")
128
+ }
129
+
130
+ /// Wraps rendered HTML in a page that follows the system appearance.
131
+ /// A card that is white while everything else is dark is worse than
132
+ /// plain text.
133
+ static func page(_ body: String, css: String? = nil) -> String {
134
+ """
135
+ <!doctype html><html><head><meta charset="utf-8">
136
+ <style>\(css ?? defaultCSS)</style>
137
+ </head><body>\(body)</body></html>
138
+ """
139
+ }
140
+ }