things-api 0.18.0 → 0.19.1

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 (224) hide show
  1. package/README.md +18 -9
  2. package/deputy/AppIcon.icns +0 -0
  3. package/deputy/VERSION +1 -1
  4. package/deputy/helpers-Info.plist +4 -0
  5. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  6. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Info.plist +3 -1
  7. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  8. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/Resources/AppIcon.icns +0 -0
  9. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/_CodeSignature/CodeResources +15 -2
  10. package/deputy/prebuilt/Things API Helper.app/Contents/Info.plist +5 -1
  11. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  12. package/deputy/prebuilt/Things API Helper.app/Contents/Resources/AppIcon.icns +0 -0
  13. package/deputy/prebuilt/Things API Helper.app/Contents/_CodeSignature/CodeResources +14 -2
  14. package/deputy/reader/Info.plist +2 -0
  15. package/deputy/reader/entitlements.plist +30 -0
  16. package/deputy/reader/main.swift +129 -40
  17. package/deputy/src/server.swift +25 -2
  18. package/deputy/src/tcc.swift +58 -0
  19. package/dist/audit/schema.d.ts +30 -1
  20. package/dist/audit/schema.js.map +1 -1
  21. package/dist/capability.d.ts +322 -0
  22. package/dist/capability.js +706 -0
  23. package/dist/capability.js.map +1 -0
  24. package/dist/cli/commands/doctor.d.ts +13 -1
  25. package/dist/cli/commands/doctor.js +141 -5
  26. package/dist/cli/commands/doctor.js.map +1 -1
  27. package/dist/cli/commands/helpers.js +189 -51
  28. package/dist/cli/commands/helpers.js.map +1 -1
  29. package/dist/cli/commands/mcp.js +8 -0
  30. package/dist/cli/commands/mcp.js.map +1 -1
  31. package/dist/cli/commands/op-result.js +6 -0
  32. package/dist/cli/commands/op-result.js.map +1 -1
  33. package/dist/cli/commands/reads.js +47 -1
  34. package/dist/cli/commands/reads.js.map +1 -1
  35. package/dist/cli/commands/repeat-flags.js +34 -2
  36. package/dist/cli/commands/repeat-flags.js.map +1 -1
  37. package/dist/cli/commands/setup.d.ts +9 -0
  38. package/dist/cli/commands/setup.js +76 -83
  39. package/dist/cli/commands/setup.js.map +1 -1
  40. package/dist/cli/commands/writes.d.ts +1 -1
  41. package/dist/cli/commands/writes.js +311 -142
  42. package/dist/cli/commands/writes.js.map +1 -1
  43. package/dist/cli/dry-run.d.ts +1 -1
  44. package/dist/cli/dry-run.js +1 -1
  45. package/dist/cli/glyphs.d.ts +23 -4
  46. package/dist/cli/glyphs.js +46 -19
  47. package/dist/cli/glyphs.js.map +1 -1
  48. package/dist/cli/help.d.ts +1 -1
  49. package/dist/cli/help.js +49 -27
  50. package/dist/cli/help.js.map +1 -1
  51. package/dist/cli/interrupt.d.ts +54 -6
  52. package/dist/cli/interrupt.js +78 -12
  53. package/dist/cli/interrupt.js.map +1 -1
  54. package/dist/cli/main.js +8 -5
  55. package/dist/cli/main.js.map +1 -1
  56. package/dist/cli/period.d.ts +3 -1
  57. package/dist/cli/period.js +1 -1
  58. package/dist/cli/period.js.map +1 -1
  59. package/dist/cli/read-driver.js +26 -1
  60. package/dist/cli/read-driver.js.map +1 -1
  61. package/dist/cli/render.d.ts +31 -4
  62. package/dist/cli/render.js +127 -15
  63. package/dist/cli/render.js.map +1 -1
  64. package/dist/client.d.ts +23 -3
  65. package/dist/client.js +36 -13
  66. package/dist/client.js.map +1 -1
  67. package/dist/contracts.d.ts +8 -4
  68. package/dist/contracts.js +1 -1
  69. package/dist/contracts.js.map +1 -1
  70. package/dist/deputy/install.d.ts +234 -6
  71. package/dist/deputy/install.js +843 -31
  72. package/dist/deputy/install.js.map +1 -1
  73. package/dist/deputy/notices.js +2 -2
  74. package/dist/deputy/notices.js.map +1 -1
  75. package/dist/deputy/protocol.d.ts +59 -5
  76. package/dist/deputy/protocol.js +48 -11
  77. package/dist/deputy/protocol.js.map +1 -1
  78. package/dist/deputy/routing.d.ts +37 -0
  79. package/dist/deputy/routing.js +181 -27
  80. package/dist/deputy/routing.js.map +1 -1
  81. package/dist/deputy/wake.d.ts +61 -0
  82. package/dist/deputy/wake.js +149 -0
  83. package/dist/deputy/wake.js.map +1 -0
  84. package/dist/diagnose.d.ts +19 -4
  85. package/dist/diagnose.js +64 -8
  86. package/dist/diagnose.js.map +1 -1
  87. package/dist/direct-setup.d.ts +104 -0
  88. package/dist/direct-setup.js +658 -0
  89. package/dist/direct-setup.js.map +1 -0
  90. package/dist/host-access.d.ts +75 -0
  91. package/dist/host-access.js +177 -0
  92. package/dist/host-access.js.map +1 -0
  93. package/dist/index.d.ts +20 -5
  94. package/dist/index.js +14 -1
  95. package/dist/index.js.map +1 -1
  96. package/dist/mcp/server.d.ts +27 -1
  97. package/dist/mcp/server.js +308 -109
  98. package/dist/mcp/server.js.map +1 -1
  99. package/dist/op-result.d.ts +9 -0
  100. package/dist/op-result.js +13 -1
  101. package/dist/op-result.js.map +1 -1
  102. package/dist/read/filter-contract.d.ts +1 -1
  103. package/dist/read/filter-contract.js +4 -0
  104. package/dist/read/filter-contract.js.map +1 -1
  105. package/dist/read/shape.js +4 -0
  106. package/dist/read/shape.js.map +1 -1
  107. package/dist/read/views.d.ts +29 -0
  108. package/dist/read/views.js +60 -0
  109. package/dist/read/views.js.map +1 -1
  110. package/dist/session-grant.d.ts +48 -0
  111. package/dist/session-grant.js +185 -0
  112. package/dist/session-grant.js.map +1 -0
  113. package/dist/surface-copy.d.ts +19 -3
  114. package/dist/surface-copy.js +19 -3
  115. package/dist/surface-copy.js.map +1 -1
  116. package/dist/wizard.d.ts +58 -0
  117. package/dist/wizard.js +174 -0
  118. package/dist/wizard.js.map +1 -0
  119. package/dist/write/availability.d.ts +1 -16
  120. package/dist/write/availability.js +10 -65
  121. package/dist/write/availability.js.map +1 -1
  122. package/dist/write/batch.d.ts +20 -0
  123. package/dist/write/batch.js +111 -21
  124. package/dist/write/batch.js.map +1 -1
  125. package/dist/write/capabilities.d.ts +9 -0
  126. package/dist/write/capabilities.js +2 -0
  127. package/dist/write/capabilities.js.map +1 -1
  128. package/dist/write/clear-reminder.js +1 -1
  129. package/dist/write/clear-reminder.js.map +1 -1
  130. package/dist/write/commands.d.ts +22 -7
  131. package/dist/write/commands.js +231 -254
  132. package/dist/write/commands.js.map +1 -1
  133. package/dist/write/failure-hints.d.ts +32 -7
  134. package/dist/write/failure-hints.js +9 -14
  135. package/dist/write/failure-hints.js.map +1 -1
  136. package/dist/write/guards.js +22 -4
  137. package/dist/write/guards.js.map +1 -1
  138. package/dist/write/lock.d.ts +16 -13
  139. package/dist/write/lock.js +55 -0
  140. package/dist/write/lock.js.map +1 -1
  141. package/dist/write/make-repeating-project.js +9 -13
  142. package/dist/write/make-repeating-project.js.map +1 -1
  143. package/dist/write/move.js +9 -12
  144. package/dist/write/move.js.map +1 -1
  145. package/dist/write/operations.d.ts +51 -29
  146. package/dist/write/operations.js +16 -0
  147. package/dist/write/operations.js.map +1 -1
  148. package/dist/write/opid.d.ts +72 -14
  149. package/dist/write/opid.js +152 -8
  150. package/dist/write/opid.js.map +1 -1
  151. package/dist/write/param-schema.d.ts +80 -0
  152. package/dist/write/param-schema.js +754 -0
  153. package/dist/write/param-schema.js.map +1 -0
  154. package/dist/write/pipeline.d.ts +110 -5
  155. package/dist/write/pipeline.js +288 -14
  156. package/dist/write/pipeline.js.map +1 -1
  157. package/dist/write/pre-state.d.ts +38 -16
  158. package/dist/write/pre-state.js +117 -22
  159. package/dist/write/pre-state.js.map +1 -1
  160. package/dist/write/promote-clone.js +658 -444
  161. package/dist/write/promote-clone.js.map +1 -1
  162. package/dist/write/reorder.js +25 -8
  163. package/dist/write/reorder.js.map +1 -1
  164. package/dist/write/repeat-collateral.d.ts +31 -0
  165. package/dist/write/repeat-collateral.js +153 -0
  166. package/dist/write/repeat-collateral.js.map +1 -0
  167. package/dist/write/repeat-rule.d.ts +28 -1
  168. package/dist/write/repeat-rule.js +54 -4
  169. package/dist/write/repeat-rule.js.map +1 -1
  170. package/dist/write/resolution-timestamps.d.ts +11 -0
  171. package/dist/write/resolution-timestamps.js +108 -40
  172. package/dist/write/resolution-timestamps.js.map +1 -1
  173. package/dist/write/reversibility.js +4 -0
  174. package/dist/write/reversibility.js.map +1 -1
  175. package/dist/write/template-mutation.d.ts +137 -0
  176. package/dist/write/template-mutation.js +598 -0
  177. package/dist/write/template-mutation.js.map +1 -0
  178. package/dist/write/undo.js +10 -20
  179. package/dist/write/undo.js.map +1 -1
  180. package/dist/write/update-fields.d.ts +151 -0
  181. package/dist/write/update-fields.js +427 -0
  182. package/dist/write/update-fields.js.map +1 -0
  183. package/dist/write/vectors/applescript.js +6 -10
  184. package/dist/write/vectors/applescript.js.map +1 -1
  185. package/dist/write/vectors/registry.d.ts +1 -1
  186. package/dist/write/vectors/registry.js +10 -2
  187. package/dist/write/vectors/registry.js.map +1 -1
  188. package/dist/write/vectors/shortcuts.js +2 -2
  189. package/dist/write/vectors/shortcuts.js.map +1 -1
  190. package/dist/write/vectors/simulator.js +15 -4
  191. package/dist/write/vectors/simulator.js.map +1 -1
  192. package/dist/write/vectors/types.d.ts +268 -1
  193. package/dist/write/vectors/ui-certification.js +63 -13
  194. package/dist/write/vectors/ui-certification.js.map +1 -1
  195. package/dist/write/vectors/ui-chord.d.ts +144 -0
  196. package/dist/write/vectors/ui-chord.js +428 -0
  197. package/dist/write/vectors/ui-chord.js.map +1 -0
  198. package/dist/write/vectors/ui-drag.d.ts +8 -4
  199. package/dist/write/vectors/ui-drag.js +2 -0
  200. package/dist/write/vectors/ui-drag.js.map +1 -1
  201. package/dist/write/vectors/ui-recipes.d.ts +70 -3
  202. package/dist/write/vectors/ui-recipes.js +482 -44
  203. package/dist/write/vectors/ui-recipes.js.map +1 -1
  204. package/dist/write/vectors/ui.d.ts +280 -10
  205. package/dist/write/vectors/ui.js +1068 -77
  206. package/dist/write/vectors/ui.js.map +1 -1
  207. package/dist/write/vectors/url-scheme.js +3 -0
  208. package/dist/write/vectors/url-scheme.js.map +1 -1
  209. package/dist/write/verify/delta.d.ts +62 -0
  210. package/dist/write/verify/delta.js +36 -2
  211. package/dist/write/verify/delta.js.map +1 -1
  212. package/dist/write/verify/poller.d.ts +4 -1
  213. package/dist/write/verify/poller.js +14 -0
  214. package/dist/write/verify/poller.js.map +1 -1
  215. package/package.json +2 -1
  216. package/schema/envelope.schema.json +6 -4
  217. package/scripts/build-helpers.sh +5 -0
  218. package/skills/things-cli/SKILL.md +5 -3
  219. package/skills/things-cli/references/contracts.md +1 -0
  220. package/skills/things-cli/references/data-model.md +3 -0
  221. package/skills/things-cli/references/errors.md +1 -1
  222. package/skills/things-cli/references/gui.md +1 -0
  223. package/skills/things-cli/references/repeating.md +6 -0
  224. package/skills/things-cli/references/tag-cleanup.md +64 -0
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
+ <img src="assets/things-api-icon-160x160.png" alt="" width="80" height="80">
2
+
1
3
  # things-api
2
4
 
3
5
  A typed TypeScript library + CLI (`things`) for programmatic interaction with [Things 3](https://culturedcode.com/things/) by Cultured Code.
4
6
 
5
- **Status: read + write + MCP layers live and published to npm (v0.18.0 — see [CHANGELOG.md](CHANGELOG.md)).** Reads go straight to the local SQLite database (UI-exact Today ordering, sidebar-grouped Anytime/Someday with container-status cascade, decoded repeat rules, occurrence projections); writes run a verified pipeline over four write vectors — two lab-validated headless surfaces (the URL scheme + AppleScript) and two opt-in surfaces (Apple Shortcuts and an Accessibility-driven GUI vector) — with hazard guards, disruption-tier policy, a JSONL audit trail, batch mode, audit-replay undo (transactional across compound operations), full project lifecycle (complete/cancel/reopen/restore), heading rename/archive/unarchive with children policies, container detach, granular stateful checklists, tag hierarchy management incl. un-nesting, completion/creation backdating, Logbook imports, tiered fuzzy name resolution (uuid prefixes, `things:///show?id=` share links), and kind-neutral in-place reordering within any container or view (one `things reorder <refs…> [--start|--end|--before|--after] --in <target>` verb — Today/Evening, a project, an area, a heading's children, someday, inbox — plus `project move-heading` for the headings themselves and `area reorder` for the sidebar). The Apple Shortcuts surface is wired for the two headless capabilities nothing else can do: creating a heading in an existing project (`things project add-heading`) and clearing a reminder from a date-scheduled item (`things todo clear-reminder`). Both run through bundled proxy shortcuts (`things setup shortcuts`) and are gated on their presence. The fourth vector — the Accessibility-driven GUI vector — is off by default and drives the local Things app to reach transforms that exist on no scriptable surface (repeat-rule editing on existing to-dos and projects, convert-to-project, sidebar area reorder); see [Accessibility GUI vector](#accessibility-gui-vector-optional-off-by-default) below. Single-item permanent delete stays interactive-only (its macOS consent has no always-allow) and is out of the headless pipeline. See [docs/design/](docs/design/) for the architecture and VM-lab design, [docs/lab/](docs/lab/harness.md) for the probe harness and campaign results the write layer is grounded in, and [docs/atlas/](docs/atlas/schema-v26.md) for the database↔UI map.
7
+ **Status: read + write + MCP layers live and published to npm (v0.19.1 — see [CHANGELOG.md](CHANGELOG.md)).** Reads go straight to the local SQLite database (UI-exact Today ordering, sidebar-grouped Anytime/Someday with container-status cascade, decoded repeat rules, occurrence projections); writes run a verified pipeline over four write vectors — two lab-validated headless surfaces (the URL scheme + AppleScript) and two opt-in surfaces (Apple Shortcuts and an Accessibility-driven GUI vector) — with hazard guards, disruption-tier policy, a JSONL audit trail, batch mode, audit-replay undo (transactional across compound operations), full project lifecycle (complete/cancel/reopen/restore), heading rename/archive/unarchive with children policies, container detach, granular stateful checklists, tag hierarchy management incl. un-nesting, completion/creation backdating, Logbook imports, tiered fuzzy name resolution (uuid prefixes, `things:///show?id=` share links), and kind-neutral in-place reordering within any container or view (one `things reorder <refs…> [--start|--end|--before|--after] --in <target>` verb — Today/Evening, a project, an area, a heading's children, someday, inbox — plus `project move-heading` for the headings themselves and `area reorder` for the sidebar). The Apple Shortcuts surface is wired for the two headless capabilities nothing else can do: creating a heading in an existing project (`things project add-heading`) and clearing a reminder from a date-scheduled item (`things todo clear-reminder`). Both run through bundled proxy shortcuts (`things setup`) and are gated on their presence. The fourth vector — the Accessibility-driven GUI vector — is off by default and drives the local Things app to reach transforms that exist on no scriptable surface (repeat-rule editing on existing to-dos and projects, convert-to-project, sidebar area reorder); see [Accessibility GUI vector](#accessibility-gui-vector-optional-off-by-default) below. Single-item permanent delete stays interactive-only (its macOS consent has no always-allow) and is out of the headless pipeline. See [docs/design/](docs/design/) for the architecture and VM-lab design, [docs/lab/](docs/lab/harness.md) for the probe harness and campaign results the write layer is grounded in, and [docs/atlas/](docs/atlas/schema-v26.md) for the database↔UI map.
6
8
 
7
9
  ```sh
8
10
  things today --json # read: your Today list, Evening split, UI order
@@ -24,19 +26,26 @@ Most ways to script Things fire a URL or an AppleScript command and hope. This p
24
26
 
25
27
  ## Requirements & first-run setup
26
28
 
27
- Things 3 installed and launched once, Node ≥ 24, and a handful of one-time macOS consents / Things settings depending on what you use (file-access consent for reads; "Enable Things URLs" + Automation consents for writes; an Accessibility grant for the optional GUI vector). **See [docs/setup.md](docs/setup.md)** — including the dedicated-automation-Mac checklist. `things doctor` validates your setup and prints remediation for anything missing.
28
-
29
- ### Shortcuts setup (optional)
29
+ Things 3 installed and launched once, Node ≥ 24, and a handful of one-time macOS consents / Things settings depending on what you use. **See [docs/setup.md](docs/setup.md)** — including the dedicated-automation-Mac checklist.
30
30
 
31
- A few operations exist on no other app surface: creating a heading in an **existing** project, clearing a reminder from a date-scheduled item, and permanently deleting a single item. These run through six bundled Apple Shortcuts (signed `.shortcut` files shipped with the package). Install them with:
31
+ Run **one** of the two setup ceremonies and you are done:
32
32
 
33
33
  ```sh
34
- things setup shortcuts # opens an install sheet per missing shortcut — click "Add Shortcut"
34
+ things setup # grants attach to this terminal (or agent harness)
35
+ things helpers setup # grants attach to a signed helper pair instead
35
36
  ```
36
37
 
37
- On each shortcut's first run macOS asks for permission choose **Always Allow** so later runs are unattended (the two delete shortcuts re-ask on every run by design; Apple offers no always-allow for deletion). `things setup shortcuts --check` and `things doctor` report installation state.
38
+ Both settle everything in one sitting read access to the Things data folder, permission to control the Things app, and the bundled shortcuts skipping anything already granted, resuming exactly where they left off, and exiting nonzero while anything is outstanding. `things setup` is the quick path; `things helpers setup` is the durable one, because the grants stop being re-requested every time your terminal updates. Which one you have is reported by `things doctor` and summarized in `things --help`.
39
+
40
+ Outside those two commands **nothing in this package will ever put a macOS permission dialog on your screen**: every entry point works out what it is allowed to do using checks that cannot prompt, and refuses with the exact remediation instead. That is a hard contract, described in [docs/design/permissions-doctrine.md](docs/design/permissions-doctrine.md) — a dialog raised anywhere else is a bug. Handing an explicit `--db <path>` (a Desktop copy, a backup) opts out of all of it: plain file semantics, ordinary errors.
41
+
42
+ ### The bundled Shortcuts
43
+
44
+ A few operations exist on no other app surface: creating a heading in an **existing** project, clearing a reminder from a date-scheduled item, and permanently deleting a single item. These run through six bundled Apple Shortcuts (signed `.shortcut` files shipped with the package), installed as one leg of `things setup` — it opens an install sheet per missing shortcut; click "Add Shortcut" on each.
45
+
46
+ On each shortcut's first run macOS asks for permission — choose **Always Allow** so later runs are unattended (the two delete shortcuts re-ask on every run by design; Apple offers no always-allow for deletion). `things setup --dry-run` and `things doctor` report installation state without asking for anything.
38
47
 
39
- Once installed, these Shortcuts-only operations become available as ordinary commands: `things project add-heading <project> <title>` (create a heading in an existing project) and `things todo clear-reminder <uuid>` (clear a date-scheduled to-do's reminder while keeping its date). Both are also exposed over MCP (the `heading` tool's add-heading action, and `clear_reminder`). If a required shortcut is missing, the command is blocked up front with a pointer back to `things setup shortcuts` — nothing is dispatched.
48
+ Once installed, these Shortcuts-only operations become available as ordinary commands: `things project add-heading <project> <title>` (create a heading in an existing project) and `things todo clear-reminder <uuid>` (clear a date-scheduled to-do's reminder while keeping its date). Both are also exposed over MCP (the `heading` tool's add-heading action, and `clear_reminder`). If a required shortcut is missing, the command is blocked up front with a pointer back to `things setup` — nothing is dispatched.
40
49
 
41
50
  ### Accessibility GUI vector (optional, off by default)
42
51
 
@@ -141,6 +150,6 @@ The catalog is consolidated around a few verb-parameterized tools (a discriminat
141
150
  - **Create**: `add_area`, `add_tag`.
142
151
  - **Headings & reminders** (Shortcuts-backed where headless-impossible): `heading` (`action` create / rename / archive / unarchive / convert_to_project), `clear_reminder`.
143
152
  - **Recurrence via the Accessibility GUI ("ui") vector** (two-key gated — `ui-enabled` config + `dangerously_drive_gui` per call): `repeat` (`scope` todo / project; `action` start / reschedule / pause / resume / create). The `heading` convert_to_project action and the `reorder` areas scope ride the same gated GUI surface.
144
- - **Generic & discovery**: `run_operation` (the full 56-op catalog), `batch`, `reorder` (item ordering across scopes, plus `scope=areas` for the sidebar area order), `undo`, `capabilities`, `doctor`.
153
+ - **Generic & discovery**: `run_operation` (the full 57-op catalog), `batch`, `reorder` (item ordering across scopes, plus `scope=areas` for the sidebar area order), `undo`, `capabilities`, `doctor`.
145
154
 
146
155
  Every write tool takes `dry_run`; the GUI-driven variants additionally require `dangerously_drive_gui`; tools carry read-only/destructive annotations; hazard blocks come back as structured tool errors carrying the same remediation text the CLI prints. Tool descriptions follow the consumer-voice contract in [docs/design/surface-copy.md](docs/design/surface-copy.md).
Binary file
package/deputy/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.3.0
@@ -17,6 +17,10 @@
17
17
  <string>APPL</string>
18
18
  <key>CFBundleShortVersionString</key>
19
19
  <string>0.0.0</string>
20
+ <!-- Original artwork, drawn by scripts/generate-helper-icon.jxa — the icon
21
+ System Settings shows next to the Automation/Accessibility toggles. -->
22
+ <key>CFBundleIconFile</key>
23
+ <string>AppIcon</string>
20
24
  <key>LSUIElement</key>
21
25
  <true/>
22
26
  </dict>
@@ -12,8 +12,10 @@
12
12
  <string>things-reader</string>
13
13
  <key>CFBundlePackageType</key>
14
14
  <string>APPL</string>
15
+ <key>CFBundleIconFile</key>
16
+ <string>AppIcon</string>
15
17
  <key>CFBundleShortVersionString</key>
16
- <string>1.1.0</string>
18
+ <string>1.3.0</string>
17
19
  <key>LSUIElement</key>
18
20
  <true/>
19
21
  </dict>
@@ -3,9 +3,22 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>files</key>
6
- <dict/>
6
+ <dict>
7
+ <key>Resources/AppIcon.icns</key>
8
+ <data>
9
+ 9VX5ZsSdgBEnhGRQvzKyEjHvzeM=
10
+ </data>
11
+ </dict>
7
12
  <key>files2</key>
8
- <dict/>
13
+ <dict>
14
+ <key>Resources/AppIcon.icns</key>
15
+ <dict>
16
+ <key>hash2</key>
17
+ <data>
18
+ li4eVDJT2l1+UncvB0nWNrYjSQliN87HTz3w1j2kG3A=
19
+ </data>
20
+ </dict>
21
+ </dict>
9
22
  <key>rules</key>
10
23
  <dict>
11
24
  <key>^Resources/</key>
@@ -16,7 +16,11 @@
16
16
  <key>CFBundlePackageType</key>
17
17
  <string>APPL</string>
18
18
  <key>CFBundleShortVersionString</key>
19
- <string>1.1.0</string>
19
+ <string>1.3.0</string>
20
+ <!-- Original artwork, drawn by scripts/generate-helper-icon.jxa — the icon
21
+ System Settings shows next to the Automation/Accessibility toggles. -->
22
+ <key>CFBundleIconFile</key>
23
+ <string>AppIcon</string>
20
24
  <key>LSUIElement</key>
21
25
  <true/>
22
26
  </dict>
@@ -3,18 +3,30 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>files</key>
6
- <dict/>
6
+ <dict>
7
+ <key>Resources/AppIcon.icns</key>
8
+ <data>
9
+ 9VX5ZsSdgBEnhGRQvzKyEjHvzeM=
10
+ </data>
11
+ </dict>
7
12
  <key>files2</key>
8
13
  <dict>
9
14
  <key>Helpers/things-reader.app</key>
10
15
  <dict>
11
16
  <key>cdhash</key>
12
17
  <data>
13
- ya8H6K5Zoo9Zq8pTP3Aum+20ZI0=
18
+ DCwtQKeK2+xh4Jh2U4b6UU9KZv4=
14
19
  </data>
15
20
  <key>requirement</key>
16
21
  <string>identifier "com.pixelcog.things-reader" and anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* exists */ and certificate leaf[subject.OU] = VNJWARH2W7</string>
17
22
  </dict>
23
+ <key>Resources/AppIcon.icns</key>
24
+ <dict>
25
+ <key>hash2</key>
26
+ <data>
27
+ li4eVDJT2l1+UncvB0nWNrYjSQliN87HTz3w1j2kG3A=
28
+ </data>
29
+ </dict>
18
30
  </dict>
19
31
  <key>rules</key>
20
32
  <dict>
@@ -12,6 +12,8 @@
12
12
  <string>things-reader</string>
13
13
  <key>CFBundlePackageType</key>
14
14
  <string>APPL</string>
15
+ <key>CFBundleIconFile</key>
16
+ <string>AppIcon</string>
15
17
  <key>CFBundleShortVersionString</key>
16
18
  <string>0.0.0</string>
17
19
  <key>LSUIElement</key>
@@ -11,5 +11,35 @@
11
11
  <true/>
12
12
  <key>com.apple.security.files.bookmarks.app-scope</key>
13
13
  <true/>
14
+ <!-- The ONE path outside the container the reader may touch: its rendezvous
15
+ directory, where it binds the client-facing socket. launchd Sockets
16
+ activation is unusable from inside the sandbox (error 159, measured
17
+ 2026-08-24), so the reader binds the socket itself under this exception.
18
+ Scoped to a single dot-directory subtree; notarization accepts
19
+ temporary-exceptions on Developer ID builds. -->
20
+ <!-- MEASURED 2026-08-24: with the file exception alone, plain file writes in
21
+ the rendezvous dir SUCCEED while bind(2) is denied EPERM — unix-socket
22
+ binding is the sandbox's network-bind operation class, path-filtered to
23
+ the container, and network.server does not extend it. The raw-SBPL
24
+ temporary exception below is the one public lever that does. -->
25
+ <!-- Unix-socket bind is the sandbox's network-bind operation class, path-
26
+ filtered to the container — MEASURED 2026-08-24: with the file exception
27
+ alone, file writes in the rendezvous dir succeed while bind(2) is EPERM,
28
+ and `network.server` does not extend it. This raw-SBPL exception is the
29
+ one public lever that does. Grammar is picky (also measured): a
30
+ `(subpath ...)` filter compiles; a regex filter and the bare
31
+ `(allow network-bind network-listen)` form both make sandbox
32
+ initialization TRAP at spawn (SIGTRAP, and launchd bootstrap fails 5).
33
+ "/Users" because entitlements are baked at signing and must serve any
34
+ user's home; the reader's practical reach stays scoped by the file
35
+ exception above (it can only create the socket where it can write). -->
36
+ <key>com.apple.security.temporary-exception.sbpl</key>
37
+ <array>
38
+ <string>(allow network-bind (subpath "/Users"))</string>
39
+ </array>
40
+ <key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
41
+ <array>
42
+ <string>/.local/state/things-api/reader/</string>
43
+ </array>
14
44
  </dict>
15
45
  </plist>
@@ -18,11 +18,35 @@
18
18
  *
19
19
  * Ships as a minimal .app bundle (secinit refuses bare executables) signed
20
20
  * with a real certificate chain (amfid refuses ad-hoc on sandboxed code).
21
- * State (bookmark, token, socket, log) lives in the sandbox container home.
21
+ *
22
+ * ── The rendezvous lives OUTSIDE this container (helpers 1.3.0) ─────────────
23
+ *
24
+ * A sandboxed process can only bind a socket inside its own container home, so
25
+ * for as long as this process bound its own socket, every CLIENT stat/open of
26
+ * that socket was a cross-app container access — the
27
+ * kTCCServiceSystemPolicyAppData consent class, silent under a Full-Disk-Access
28
+ * host and a modal from anywhere else. That made reader routing per-host, which
29
+ * is the opposite of what the helper pair exists for.
30
+ *
31
+ * So the rendezvous lives OUTSIDE the container, in the user's own state dir:
32
+ *
33
+ * • the SOCKET is bound by THIS process at the host-neutral path injected via
34
+ * THINGS_READER_SOCKET — the sandbox permits it through a
35
+ * home-relative-path temporary-exception entitlement scoped to that one
36
+ * directory. (launchd `Sockets` activation was tried first and FAILS from
37
+ * inside the App Sandbox with error 159 "Sandbox restriction", measured
38
+ * 2026-08-24 — do not re-attempt it.) No fallback paths: without the env
39
+ * this process exits, loudly;
40
+ * • the TOKEN it expects arrives in THINGS_READER_TOKEN, injected into the
41
+ * same plist by the installer, which writes the matching file for clients.
42
+ *
43
+ * What stays in the container is what was always right to keep there: the
44
+ * security-scoped BOOKMARK (the read grant itself, reader-internal, never read
45
+ * by a client) and the audit log.
22
46
  *
23
47
  * Modes:
24
48
  * --grant <startDir> present the NSOpenPanel ceremony, save the bookmark
25
- * --serve serve the socket (launchd mode; default)
49
+ * --serve bind and serve the rendezvous socket (default)
26
50
  * --version print the version
27
51
  */
28
52
  import AppKit
@@ -34,17 +58,48 @@ let READER_PROTOCOL_VERSION = 1
34
58
  let MAX_LINE_BYTES = 8 * 1024 * 1024
35
59
  let MAX_FILE_READ_BYTES = 64 * 1024 * 1024
36
60
 
61
+ /// The env var the LaunchAgent carries the expected access token in.
62
+ let READER_TOKEN_ENV = "THINGS_READER_TOKEN"
63
+
37
64
  // Sandboxed home == the container Data dir — the durable private store.
38
65
  let home = NSHomeDirectory()
39
66
  let bookmarkFile = home + "/things-reader.bookmark"
40
- let socketPath = home + "/reader.sock"
41
- let tokenFile = home + "/token"
42
67
  let logFile = home + "/reader.log"
68
+ // Pre-1.3.0 the socket and token lived here. Only this process can delete them
69
+ // (they are inside its container), so it does — see cleanUpLegacyRendezvous().
70
+ let legacySocketPath = home + "/reader.sock"
71
+ let legacyTokenFile = home + "/token"
43
72
 
44
73
  func stderrLine(_ message: String) {
45
74
  FileHandle.standardError.write(Data("things-reader: \(message)\n".utf8))
46
75
  }
47
76
 
77
+ /**
78
+ * The rendezvous socket path, injected by the installer into the LaunchAgent.
79
+ * launchd socket activation is NOT usable here — MEASURED 2026-08-24 on this
80
+ * exact bundle: `launch_activate_socket` fails with 159 ("Sandbox restriction")
81
+ * from inside the App Sandbox, so the reader binds the socket ITSELF at a
82
+ * host-neutral path its entitlements grant
83
+ * (the home-relative-path file exception for the rendezvous dir, plus the
84
+ * raw-SBPL `(allow network-bind (subpath "/Users"))` — unix bind is the
85
+ * sandbox's network-bind class, which file exceptions alone do not grant;
86
+ * see entitlements.plist for the measured grammar). Clients reach the socket
87
+ * with no TCC class.
88
+ */
89
+ let READER_SOCKET_ENV = "THINGS_READER_SOCKET"
90
+
91
+ /**
92
+ * Delete the pre-1.3.0 in-container rendezvous. Those files are dead the moment
93
+ * this build runs — nothing binds or reads them any more — and this process is
94
+ * the ONLY one that can remove them, since they sit in its sandbox container
95
+ * and a client touching that path is the very consent class 1.3.0 escaped.
96
+ * Idempotent; a fresh install finds nothing.
97
+ */
98
+ func cleanUpLegacyRendezvous() {
99
+ unlink(legacySocketPath)
100
+ unlink(legacyTokenFile)
101
+ }
102
+
48
103
  // --- grant ceremony ---
49
104
 
50
105
  func runGrant(startDir: String) -> Never {
@@ -126,19 +181,6 @@ let scope = Scope()
126
181
 
127
182
  // --- server (file verbs only) ---
128
183
 
129
- func loadOrCreateToken() -> String {
130
- if let data = FileManager.default.contents(atPath: tokenFile) {
131
- let tok = String(decoding: data, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines)
132
- if !tok.isEmpty { return tok }
133
- }
134
- var bytes = [UInt8](repeating: 0, count: 32)
135
- for i in 0..<bytes.count { bytes[i] = UInt8.random(in: 0...255) }
136
- let tok = bytes.map { String(format: "%02x", $0) }.joined()
137
- FileManager.default.createFile(
138
- atPath: tokenFile, contents: Data(tok.utf8), attributes: [.posixPermissions: 0o600])
139
- return tok
140
- }
141
-
142
184
  /// How long a shutting-down reader waits for requests already in flight (the
143
185
  /// deputy's DRAIN_TIMEOUT_SECONDS; the two halves are booted out together).
144
186
  let READER_DRAIN_TIMEOUT_SECONDS: TimeInterval = 10
@@ -173,17 +215,41 @@ final class ReaderServer {
173
215
  cacheLock.unlock()
174
216
  }
175
217
 
176
- func run() {
218
+ private var socketPath: String = ""
219
+
220
+ /**
221
+ * Bind and serve the rendezvous socket at the host-neutral path the
222
+ * installer injected. The sandbox permits the bind through the reader's
223
+ * home-relative-path entitlement; launchd activation is unusable from
224
+ * inside the sandbox (error 159, measured — see the header).
225
+ */
226
+ func run(socketPath path: String) {
227
+ socketPath = path
228
+ // Boot diagnostic: discriminate a file-rule failure (entitlement form/path
229
+ // wrong) from a bind-rule failure (operation class) when the bind below
230
+ // fails. Removed file, one line, only on failure.
231
+ let probePath = (path as NSString).deletingLastPathComponent + "/.write-probe"
232
+ if FileManager.default.createFile(atPath: probePath, contents: Data("x".utf8)) {
233
+ unlink(probePath)
234
+ } else {
235
+ stderrLine("write-probe FAILED in rendezvous dir — the file entitlement is not covering it")
236
+ }
177
237
  let fd = socket(AF_UNIX, SOCK_STREAM, 0)
178
- guard fd >= 0 else { fatalDie("socket() errno \(errno)") }
238
+ guard fd >= 0 else {
239
+ stderrLine("socket() errno \(errno)")
240
+ exit(78)
241
+ }
179
242
  listenFd = fd
180
- unlink(socketPath)
243
+ unlink(path)
181
244
  var addr = sockaddr_un()
182
245
  addr.sun_family = sa_family_t(AF_UNIX)
183
246
  let capacity = MemoryLayout.size(ofValue: addr.sun_path)
184
- guard socketPath.utf8.count < capacity else { fatalDie("socket path too long: \(socketPath)") }
247
+ guard path.utf8.count < capacity else {
248
+ stderrLine("socket path too long: \(path)")
249
+ exit(78)
250
+ }
185
251
  _ = withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
186
- socketPath.withCString { cstr in
252
+ path.withCString { cstr in
187
253
  strncpy(UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: CChar.self), cstr, capacity - 1)
188
254
  }
189
255
  }
@@ -192,9 +258,16 @@ final class ReaderServer {
192
258
  bind(fd, sa, socklen_t(MemoryLayout<sockaddr_un>.size))
193
259
  }
194
260
  }
195
- guard bound == 0 else { fatalDie("bind(\(socketPath)) errno \(errno)") }
196
- chmod(socketPath, 0o600)
197
- guard listen(fd, 16) == 0 else { fatalDie("listen() errno \(errno)") }
261
+ guard bound == 0 else {
262
+ stderrLine(
263
+ "bind(\(path)) errno \(errno) the reader's sandbox entitlement must cover this path; run `things helpers setup`")
264
+ exit(78)
265
+ }
266
+ chmod(path, 0o600)
267
+ guard listen(fd, 16) == 0 else {
268
+ stderrLine("listen() errno \(errno)")
269
+ exit(78)
270
+ }
198
271
  audit(["event": "started", "pid": Int(getpid()), "version": DEPUTY_VERSION])
199
272
  while true {
200
273
  let conn = accept(fd, nil, nil)
@@ -214,18 +287,14 @@ final class ReaderServer {
214
287
  }
215
288
  }
216
289
 
217
- private func fatalDie(_ message: String) -> Never {
218
- stderrLine(message)
219
- exit(1)
220
- }
221
-
222
290
  /**
223
- * Graceful drain, then teardown (mirrors the deputy): remove the socket path
224
- * so no new client can find us, close the listener, wait out the in-flight
225
- * requests within a bound, flush the log. SIGKILL is still the hard stop.
291
+ * Graceful drain, then teardown the deputy's semantics: unlink the socket
292
+ * FIRST (the deterministic accept-wake, and it stops new clients dialing a
293
+ * dying process), finish in-flight requests within the bound, flush the log.
294
+ * SIGKILL is the hard stop.
226
295
  */
227
296
  func drainAndShutdown(timeout: TimeInterval = READER_DRAIN_TIMEOUT_SECONDS) {
228
- unlink(socketPath)
297
+ if !socketPath.isEmpty { unlink(socketPath) }
229
298
  if listenFd >= 0 {
230
299
  Darwin.shutdown(listenFd, SHUT_RDWR)
231
300
  close(listenFd)
@@ -303,7 +372,7 @@ final class ReaderServer {
303
372
  return errorResponse(id: id, code: "unsupported-protocol", message: "reader speaks protocol \(READER_PROTOCOL_VERSION)")
304
373
  }
305
374
  guard let reqToken = obj["token"] as? String, reqToken == token else {
306
- return errorResponse(id: id, code: "bad-token", message: "request token does not match the reader token file")
375
+ return errorResponse(id: id, code: "bad-token", message: "request token does not match the reader's access token")
307
376
  }
308
377
  guard let verb = obj["verb"] as? String else {
309
378
  return errorResponse(id: id, code: "bad-request", message: "missing verb")
@@ -354,7 +423,7 @@ final class ReaderServer {
354
423
  nil,
355
424
  errorResponse(
356
425
  id: id, code: "not-granted",
357
- message: "no security-scoped grant yet — run `things helpers grant` and select the Things data folder")
426
+ message: "no security-scoped grant yet — run `things helpers setup` and select the Things data folder")
358
427
  )
359
428
  }
360
429
  return (root, nil)
@@ -467,7 +536,27 @@ case "--grant":
467
536
  }
468
537
  runGrant(startDir: arguments[2])
469
538
  case "--serve":
470
- let server = ReaderServer(token: loadOrCreateToken())
539
+ // No fallbacks, deliberately: both the token and the listening socket come
540
+ // from the LaunchAgent, so a --serve outside it is a misconfiguration to
541
+ // surface, never a second code path to keep working. EX_CONFIG (78).
542
+ guard let token = ProcessInfo.processInfo.environment[READER_TOKEN_ENV], !token.isEmpty else {
543
+ stderrLine(
544
+ "\(READER_TOKEN_ENV) is not set — the access token is injected by the reader's LaunchAgent; run `things helpers setup`")
545
+ exit(78)
546
+ }
547
+ guard let socketPath = ProcessInfo.processInfo.environment[READER_SOCKET_ENV],
548
+ !socketPath.isEmpty
549
+ else {
550
+ stderrLine(
551
+ "\(READER_SOCKET_ENV) is not set — the rendezvous path is injected by the reader's LaunchAgent; run `things helpers setup`")
552
+ exit(78)
553
+ }
554
+ // 1.3.0 migration, and only on a real boot — the refusals above must stay
555
+ // side-effect-free, so nothing is deleted until launchd has actually
556
+ // activated us. Idempotent, so paying it every boot costs two failed
557
+ // unlink(2)s.
558
+ cleanUpLegacyRendezvous()
559
+ let server = ReaderServer(token: token)
471
560
  signal(SIGPIPE, SIG_IGN)
472
561
  signal(SIGTERM, SIG_IGN)
473
562
  signal(SIGINT, SIG_IGN)
@@ -476,14 +565,14 @@ case "--serve":
476
565
  for source in [termSource, intSource] {
477
566
  source.setEventHandler {
478
567
  // Graceful drain: stop accepting, finish in-flight reads within a bound,
479
- // then remove the socket and exit cleanly (an upgrade boots both halves
480
- // out mid-flight).
568
+ // then exit cleanly (an upgrade boots both halves out mid-flight). The
569
+ // socket itself is launchd's and outlives us.
481
570
  server.drainAndShutdown()
482
571
  exit(0)
483
572
  }
484
573
  source.resume()
485
574
  }
486
- Thread.detachNewThread { server.run() }
575
+ Thread.detachNewThread { server.run(socketPath: socketPath) }
487
576
  dispatchMain()
488
577
  default:
489
578
  stderrLine("usage: things-reader --serve | --grant <dir> | --version")
@@ -12,7 +12,7 @@
12
12
  import Foundation
13
13
 
14
14
  /// How long a shutting-down deputy waits for requests already in flight. An
15
- /// upgrade (`things helpers install` / `restart`) boots the old process out
15
+ /// upgrade (`things helpers setup` / `restart`) boots the old process out
16
16
  /// mid-flight; without a drain the in-flight request dies with it. launchd's
17
17
  /// own bootout wait, and the CLI's `launchctl` timeout, both clear this bound.
18
18
  let DRAIN_TIMEOUT_SECONDS: TimeInterval = 10
@@ -212,6 +212,8 @@ final class Server {
212
212
  switch verb {
213
213
  case "hello":
214
214
  result = handleHello(id: id)
215
+ case "prime-ax":
216
+ result = handlePrimeAx(id: id)
215
217
  case "osascript":
216
218
  guard let script = obj["script"] as? String else {
217
219
  result = errorResponse(id: id, code: "bad-request", message: "osascript verb requires a script string")
@@ -275,7 +277,7 @@ final class Server {
275
277
  // File verbs live exclusively on the sandboxed things-reader.
276
278
  result = errorResponse(
277
279
  id: id, code: "unsupported-verb",
278
- message: "the deputy serves automation verbs only — file access rides things-reader (things helpers grant)")
280
+ message: "the deputy serves automation verbs only — file access rides things-reader (things helpers setup)")
279
281
  default:
280
282
  result = errorResponse(id: id, code: "bad-request", message: "unknown verb \(verb)")
281
283
  }
@@ -312,6 +314,13 @@ final class Server {
312
314
 
313
315
  // --- verb handlers ---
314
316
 
317
+ /**
318
+ * The handshake, plus the deputy's own TCC standing — Accessibility trust
319
+ * and Automation permission for each target it drives. Every field here is
320
+ * PROMPT-FREE by construction (see tcc.swift), which is what lets a status
321
+ * report and the onboarding ceremony ask "is this leg already granted?"
322
+ * without raising a dialog at whoever happens to run `things` next.
323
+ */
315
324
  private func handleHello(id: Any?) -> [String: Any] {
316
325
  [
317
326
  "id": id ?? NSNull(),
@@ -321,9 +330,23 @@ final class Server {
321
330
  "role": "deputy",
322
331
  "pid": Int(getpid()),
323
332
  "uptimeMs": Int(Date().timeIntervalSince(startedAt) * 1000),
333
+ "axTrusted": accessibilityTrusted(),
334
+ "automation": [
335
+ "things": automationStatus(bundleID: "com.culturedcode.ThingsMac"),
336
+ "systemEvents": automationStatus(bundleID: "com.apple.systemevents"),
337
+ ],
324
338
  ]
325
339
  }
326
340
 
341
+ /**
342
+ * Raise the Accessibility consent dialog for the deputy's own identity. The
343
+ * grant itself is a toggle in System Settings, so the answer can only arrive
344
+ * later — the caller polls `hello`'s axTrusted. Returns the state as of now.
345
+ */
346
+ private func handlePrimeAx(id: Any?) -> [String: Any] {
347
+ ["id": id ?? NSNull(), "ok": true, "axTrusted": primeAccessibility()]
348
+ }
349
+
327
350
  // --- audit log ---
328
351
 
329
352
  private func audit(_ entry: [String: Any]) {
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Prompt-free TCC introspection, plus the one call that deliberately DOES
3
+ * prompt (Accessibility).
4
+ *
5
+ * The deputy is the process macOS attributes every AppleEvent and every
6
+ * Accessibility action to, so it — and only it — can answer "am I allowed to
7
+ * drive Things?" truthfully. Both answers ride the `hello` response, which
8
+ * makes `things helpers status` and the onboarding ceremony able to SKIP a leg
9
+ * that is already granted instead of re-firing a dialog at the user.
10
+ *
11
+ * `AEDeterminePermissionToAutomateTarget(…, askUserIfNeeded: false)` is the
12
+ * documented non-prompting probe: it answers from the existing TCC record and
13
+ * returns errAEEventWouldRequireUserConsent (-1744) when no record exists yet
14
+ * — reported here as "unknown", which is exactly the state the ceremony then
15
+ * resolves by sending a real (prompting) AppleEvent.
16
+ */
17
+ import ApplicationServices
18
+ import Foundation
19
+
20
+ /// One target's Automation state: "granted" | "denied" | "not-running" | "unknown".
21
+ func automationStatus(bundleID: String) -> String {
22
+ var target = AEAddressDesc()
23
+ let bytes = Array(bundleID.utf8)
24
+ let created = bytes.withUnsafeBufferPointer { buf in
25
+ AECreateDesc(typeApplicationBundleID, buf.baseAddress, buf.count, &target)
26
+ }
27
+ guard created == noErr else { return "unknown" }
28
+ defer { AEDisposeDesc(&target) }
29
+ // askUserIfNeeded: false — a status probe must never raise a dialog.
30
+ let status = AEDeterminePermissionToAutomateTarget(&target, typeWildCard, typeWildCard, false)
31
+ switch status {
32
+ case noErr:
33
+ return "granted"
34
+ case -1743: // errAEEventNotPermitted — the user (or MDM) said no.
35
+ return "denied"
36
+ case -600: // procNotFound — the target is not running, so TCC has no answer.
37
+ return "not-running"
38
+ default:
39
+ // -1744 errAEEventWouldRequireUserConsent lands here: never asked yet.
40
+ return "unknown"
41
+ }
42
+ }
43
+
44
+ /// Accessibility trust for THIS process, prompt-free.
45
+ func accessibilityTrusted() -> Bool {
46
+ AXIsProcessTrusted()
47
+ }
48
+
49
+ /**
50
+ * Ask macOS to show the Accessibility prompt for this process. The dialog is
51
+ * fire-and-forget (it offers a Settings deep-link; the actual grant is a
52
+ * toggle the user flips in System Settings), so this returns the CURRENT trust
53
+ * state immediately rather than waiting for one that can only arrive later.
54
+ */
55
+ func primeAccessibility() -> Bool {
56
+ let key = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String
57
+ return AXIsProcessTrustedWithOptions([key: true] as CFDictionary)
58
+ }