apple-tools-mcp 2.1.0 → 2.1.2
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.
- package/README.md +12 -10
- package/lib/appleScript.js +9 -5
- package/lib/mailWrite.js +164 -60
- package/lib/permissions.js +23 -11
- package/lib/writeRouting.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ Write tools drive Mail, Messages, Calendar, and Contacts through AppleScript. ma
|
|
|
77
77
|
|
|
78
78
|
#### First install and upgrade: `apple-tools-mcp permissions`
|
|
79
79
|
|
|
80
|
-
Run this **after first global install** and **after upgrade** when write surfaces are present or change. It probes **`process.execPath`** (the `node` running the command) so macOS can pop **Allow** dialogs for Contacts, Calendar, Mail, and
|
|
80
|
+
Run this **after first global install** and **after upgrade** when write surfaces are present or change. It probes **`process.execPath`** (the `node` running the command) so macOS can pop **Allow** dialogs for Contacts, Calendar, Mail, Messages, and System Events in one sitting. You click **Allow**; the command cannot grant silently. Already-granted surfaces report OK without another click. Missing grants print a report and the process **exits non-zero** (fail closed).
|
|
81
81
|
|
|
82
82
|
Use the **same `node` the product uses** — not the MCP host app. Always invoke so **`process.execPath`** is that node. A bare `apple-tools-mcp` (or a path like `~/.nvm/versions/node/v22.21.1/bin/apple-tools-mcp`) can have a shebang that starts a **different** node; Allow dialogs attach to `execPath`, not the CLI path you typed.
|
|
83
83
|
|
|
@@ -98,15 +98,15 @@ If the invoked CLI path and `process.execPath` differ, the command prints a **WA
|
|
|
98
98
|
- **Mini:** `/Users/petercoates/.local/node/bin/node` (global npm / indexer LaunchAgent)
|
|
99
99
|
- **MacBook:** `/Users/petercoates/.nvm/versions/node/v22.21.1/bin/node` (Claude’s nvm `node`, **not** Homebrew)
|
|
100
100
|
|
|
101
|
-
**MacBook (default for `permissions`):** run from **Terminal.app**. Click **Allow** for the **printed `process.execPath`**. Confirm **System Settings → Privacy & Security → Automation** for that node → Contacts, Calendar, Mail, and
|
|
101
|
+
**MacBook (default for `permissions`):** run from **Terminal.app**. Click **Allow** for the **printed `process.execPath`**. Confirm **System Settings → Privacy & Security → Automation** for that node → Contacts, Calendar, Mail, Messages, and System Events. Do **not** start `apple-tools-indexer` — the always-on indexer / write bridge is a **Mini** install only.
|
|
102
102
|
|
|
103
103
|
**Mini:** use the [Mini ship-gate](#mini-ship-gate-host-setup) path (LaunchAgent-owned node / `writer.sock`). Missing-grant copy mentions the write bridge only when that socket is present.
|
|
104
104
|
|
|
105
|
-
On the host UI (Mini Screen Sharing or MacBook local), open **System Settings → Privacy & Security → Automation**, then run the command. Click **Allow** for **`node`** → Contacts, Calendar, Mail, and
|
|
105
|
+
On the host UI (Mini Screen Sharing or MacBook local), open **System Settings → Privacy & Security → Automation**, then run the command. Click **Allow** for **`node`** → Contacts, Calendar, Mail, Messages, and System Events. npm `postinstall` only **prints a reminder** — it does not run the probes unattended.
|
|
106
106
|
|
|
107
|
-
This is a real Apple Events pass: Mail uses `make new outgoing message` (compose then discard; nothing is sent). Messages enumerates accounts (nothing is sent). `dry_run` of `mail_send` / `messages_send` never talks to those apps and **does not count**. Contacts creates and deletes a throwaway person in-script. Calendar lists calendars only (no leftover events).
|
|
107
|
+
This is a real Apple Events pass: Mail uses `make new outgoing message` (compose then discard; nothing is sent). Messages enumerates accounts (nothing is sent). `dry_run` of `mail_send` / `messages_send` never talks to those apps and **does not count**. Contacts creates and deletes a throwaway person in-script. Calendar lists calendars only (no leftover events). System Events is probed with **process / GUI scripting** (`tell process` + `unix id` of System Events; nothing is typed) so the keystroke compose path can pop **Allow** and verify a real Automation grant. A soft `tell application "System Events" to get name` is **not** enough — that can succeed while TCC still has node → System Events **denied**. If a prior **Don't Allow** left a sticky Deny, `permissions` reports System Events **missing** and exits non-zero: turn **System Events** on for that node in System Settings → Privacy & Security → **Automation**, then **re-run `apple-tools-mcp permissions` after Allow**.
|
|
108
108
|
|
|
109
|
-
**One-pass first-run:** Allow **`node`** to control **Mail**, **Messages**, **Contacts**, and **
|
|
109
|
+
**One-pass first-run:** Allow **`node`** to control **Mail**, **Messages**, **Contacts**, **Calendar**, and **System Events** in the same Automation pass. Contacts or Calendar being allowed does **not** grant Mail, Messages, or System Events — they are separate Apple Events targets. A denied Mail grant hangs `mail_send` / `mail_draft` / `mail_reply` / `mail_forward` until the client disconnects; `dry_run` never talks to Mail, so that deny is invisible until a real compose.
|
|
110
110
|
|
|
111
111
|
**Do not add `node` via the + button in System Settings → Privacy & Security → Contacts or Calendars.** On current macOS those panes often have **no Add button**, and that instruction is not the ship-gate setup — it failed on the Mini.
|
|
112
112
|
|
|
@@ -466,7 +466,7 @@ Two arguments are available on **every** write tool:
|
|
|
466
466
|
|
|
467
467
|
**Compose is not quoted.** `mail_send` / `mail_draft` (plain and html) call `make new outgoing message` **without** AppleScript `content:` so `newMessage` is a real Mail object, then type the body into the compose window (System Events keystrokes). Mail's `mailto` command was shipped in 2.0.4 and **fails on Mini/MacBook Mail**: it does not return an outgoing message (`newMessage` undefined, AppleScript **-2753**). On current Mail (Ventura+, FB11734014) `content` / `html content` store the body as a citation: every plain-text line prefixed with `>`, plus a `multipart/alternative` HTML part wrapped in `<blockquote type="cite">`. Desktop Mail often hides the bar with inline styles; iOS Mail paints the whole body purple with a left quote bar — even when the subject is not `Re:`/`Fwd:` and there is no `In-Reply-To`. Reply and forward still quote the original, which is expected (those paths still prepend via `content`; fixing reply/forward cite-wrap is out of scope for 2.1.0).
|
|
468
468
|
|
|
469
|
-
Keystrokes use System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) **and** Automation → Mail.
|
|
469
|
+
Keystrokes use System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) **and** Automation → Mail **and** Automation → System Events. Run `apple-tools-mcp permissions` after first install or upgrade so **node → System Events** pops **Allow** in the same sitting as Mail / Messages / Contacts / Calendar. An Accessibility / System Events deny is not a Mail Automation deny (`-1743` / `-10004`).
|
|
470
470
|
|
|
471
471
|
**Compose body focus (2.0.8).** After `make new outgoing message`, To and Subject are AppleScript properties (never paste). Cmd-V used to run only after an AX window walker left the header. Quote-prefix Sent prove and Sent/Outbox verify stay gated on fail-closed body/header checks — a MacBook 2.0.7 live `mail_send` with Accessibility allowed still hit `BODY_PASTE_MISDIRECTED` when Cmd-V landed in To/Subject (`ATP-207-SENT-PROVE-20260921-084048`).
|
|
472
472
|
|
|
@@ -474,11 +474,13 @@ Keystrokes use System Events, so **node needs Accessibility** (Privacy & Securit
|
|
|
474
474
|
|
|
475
475
|
**AppleScript reserved identifiers (2.0.10).** Dual-host Sent prove on `@2.0.9` compiled past the focused-UI fix, then failed in `atmFocusMailBody`: `set {tw, th} to size of atmElem` → **-2741** Expected expression but found `th` (AppleScript reserved ordinal).
|
|
476
476
|
|
|
477
|
-
**Keystroke compose (2.1.0).** macOS 26 System Events reports `windows of process Mail = 0`, so the AX window walker cannot focus a tall `AXWebArea`. Fresh compose
|
|
477
|
+
**Keystroke compose (2.1.0).** macOS 26 System Events reports `windows of process Mail = 0`, so the AX window walker cannot focus a tall `AXWebArea`. Fresh compose **Tabs into the body** (max Tab×6 as proven on Mini) and **types the body with Return between paragraphs**. Clipboard Cmd-V is only a fallback if a *readable* AX value is still missing the body needle. Fail-closed: missing AX body focus or changed To/Cc/Bcc/subject is `BODY_FOCUS_FAILED` / `BODY_PASTE_MISDIRECTED`; the outgoing message is deleted and nothing is sent. Apple events are wrapped in `with timeout`; hung `osascript` is **SIGKILL**'d so a wedge fails fast instead of sitting ~60s. If the Mini write-bridge / indexer daemon cannot drive System Events, `mail_send` returns `unsupported: true` so the stdio client falls back **in-process** — it does **not** hang with `MAIL_SEND_TIMEOUT_GUIDANCE` for a message that never sent.
|
|
478
|
+
|
|
479
|
+
**AX body-focus oracle (2.1.1).** Dual-host Mail 16 / macOS 26 showed that Mail AppleScript `content of` the outgoing message is **not** a live oracle for UI-typed/pasted text: `set content` reads back, but after Tab + keystroke/paste into the UI body `content of` stays empty (`typedSeen=false`, bodyLen 0) even when typing still produces a plain Sent `.emlx`. `@2.1.0` calibrated Tab against that property and fail-closed with `BODY_FOCUS_FAILED`. 2.1.1 Tabs until the focused AX role is a body field (`AXWebArea` / non-header `AXTextArea` via `AXFocusedUIElement` / `first UI element whose focused is true` — never `focused UI element`, never `windows of process Mail`). Cmd-V runs only when that AX **value** is readable and still missing the needle. An empty AX value is not a miss (Mail 16 WebArea); header checks still fail-closed. Ship prove that the body landed remains the Sent `.emlx` after send (2.0.7 To+subject verify). Do not “fix Tab count.”
|
|
478
480
|
|
|
479
481
|
**mail_send success is Sent/Outbox verify (2.0.7).** AppleScript `send` returning without throw is not enough. After a real send the tool looks in **Sent** and **Outbox** and reports success only if the message is there. The success text names the delivery state (`mailbox: sent` + `delivery: sent`, or `mailbox: outbox` + `delivery: outbox` while still sending). If verify misses, the tool returns a failure (`isError`) — not success. Hang/timeout recover matches **To + subject** (and Message-ID when compose captured one). It never matches subject alone (short subjects like `test` are unsafe). `from` / account selection is out of scope; Mail's default From is used. `dry_run` / confirm-blocked writes stay `ok: false`, `planned: true`, `delivered: false`.
|
|
480
482
|
|
|
481
|
-
**Manual prove (2.1.
|
|
483
|
+
**Manual prove (2.1.1 on the Mac host):** After `apple-tools-mcp permissions` reports System Events granted, `mail_send` a short **multiline** plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. The caret must land in the **body** (not To/Subject): no `BODY_FOCUS_FAILED` / `BODY_PASTE_MISDIRECTED`. Inspect each Sent `.emlx`: the text/plain part must contain the intended text (newlines preserved) and must not prefix every body line with `>`, and any HTML alternative must not wrap the whole body in `<blockquote type="cite">`. Compose must succeed (no `-2753` / undefined `newMessage`). On Mini, prove the production path (write-bridge with node → System Events allowed, **or** the documented in-process fallback). That quote-prefix dual-host Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
482
484
|
|
|
483
485
|
Emails are addressed by their RFC822 **Message-ID**. Pass `message_id`, or pass the `file_path` from `mail_search` / `mail_recent` and the server reads the Message-ID out of the `.emlx` headers for you. `mail_archive` moves the message to its account's Archive (or All Mail) mailbox; `mail_trash` moves it to that account's Trash.
|
|
484
486
|
|
|
@@ -599,9 +601,9 @@ Ensure Node.js has Full Disk Access (see Installation step 2).
|
|
|
599
601
|
|
|
600
602
|
The message names which process macOS was actually asking about. Work through it in this order:
|
|
601
603
|
|
|
602
|
-
1. **MacBook / Terminal (`permissions` CLI):** there is **no** always-on indexer. Re-run `apple-tools-mcp permissions` from **Terminal.app**, Allow the **printed `process.execPath`**, and check System Settings → Privacy & Security → **Automation** for that node → Contacts, Calendar, Mail, and
|
|
604
|
+
1. **MacBook / Terminal (`permissions` CLI):** there is **no** always-on indexer. Re-run `apple-tools-mcp permissions` from **Terminal.app**, Allow the **printed `process.execPath`**, and check System Settings → Privacy & Security → **Automation** for that node → Contacts, Calendar, Mail, Messages, and System Events. Do **not** start `apple-tools-indexer`.
|
|
603
605
|
2. **Mini only:** `pgrep -fl apple-tools-indexer` / `~/.apple-tools-mcp/writer.sock`. If the LaunchAgent is the product host, start it so writes run under launchd-owned node (see [Mini ship-gate](#mini-ship-gate-host-setup)).
|
|
604
|
-
3. **Did you approve the Automation prompts for `node`?** Re-run `apple-tools-mcp permissions` with the product `node` (it prints `process.execPath`). Open System Settings → Privacy & Security → **Automation** and confirm **that printed node** is allowed to control **Mail**, **Messages**, Contacts, and
|
|
606
|
+
3. **Did you approve the Automation prompts for `node`?** Re-run `apple-tools-mcp permissions` with the product `node` (it prints `process.execPath`). Open System Settings → Privacy & Security → **Automation** and confirm **that printed node** is allowed to control **Mail**, **Messages**, Contacts, Calendar, and **System Events**. A prior **Don't Allow** for System Events is a sticky Deny — the Allow dialog will not appear again; turn **System Events** on for that node, then re-run `permissions` (it must not report granted from a soft name check). On Mini the LaunchAgent binary is whatever the plist / `which node` reports (`/Users/petercoates/.local/node/bin/node` is a Mini *example* only). MacBook Claude nvm is `/Users/petercoates/.nvm/versions/node/v22.21.1/bin/node`, not Homebrew. Watch the host — Allow **`node`**, not any other app. Contacts or Calendar being allowed does **not** grant Mail. A `mail_send` hang is **Automation denied**, not “Mail.app could not be reached”; `dry_run` never talks to Mail so it cannot detect this. Do **not** approve the MCP client / host app that launched a short-lived stdio server, and do **not** try to add `node` via **+** in the Contacts or Calendars privacy lists — those panes often have no Add button. `tccutil reset AppleEvents` re-arms the Automation prompt so you can Allow **`node`** again. Full Disk Access is a separate read grant; npm Trusted Publisher / tokens are unrelated.
|
|
605
607
|
4. **Mini: is the daemon's node binary the one with Full Disk Access?** LaunchAgents do not inherit your shell `PATH`; confirm the plist points at the same path `which node` reports.
|
|
606
608
|
5. **Is it actually the write path?** Contacts or Calendar *reads* failing with `EPERM` is a Full Disk Access / attribution problem, not the entitlement gap — fix FDA for the responsible process. Contacts or Calendar *CRUD* failing with no prompt under Claude Desktop is the [documented host limitation](#reads-and-writes-are-different-mechanisms): Claude.app carries neither the addressbook nor the calendars entitlement. On Mini, start the daemon so the write goes through the bridge. On MacBook, use Terminal.app + the printed node.
|
|
607
609
|
6. **"Contacts.app / Calendar.app could not be reached"** with the app clearly installed is an **Automation / responsible-process** failure, not a missing app — macOS reports a refused Apple event as `-1728` / "can't get application". On MacBook / Terminal, Allow the printed `process.execPath`. On Mini, the write bridge / LaunchAgent may apply. A Contacts `-600` / “application isn't running” is a **cold launch**, not Automation — keep Contacts, Mail, and Messages running; Calendar does not need to stay open.
|
package/lib/appleScript.js
CHANGED
|
@@ -345,15 +345,19 @@ export const MAIL_ACCESSIBILITY_GUIDANCE =
|
|
|
345
345
|
"This is not a Mail Automation deny (-1743 / -10004).";
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* or sent. Mini write-bridge should return unsupported so stdio falls back
|
|
351
|
-
* in-process rather than wedging ~60s.
|
|
348
|
+
* Sticky Don't Allow for node → System Events: the Allow dialog will not
|
|
349
|
+
* reappear. Operators must flip the Automation row and re-run permissions.
|
|
352
350
|
*/
|
|
351
|
+
export const SYSTEM_EVENTS_STICKY_DENY_GUIDANCE =
|
|
352
|
+
"A prior Don't Allow for node → System Events is a sticky Deny: the Allow dialog will not appear again. " +
|
|
353
|
+
"Turn System Events on for this node in System Settings → Privacy & Security → Automation, then re-run apple-tools-mcp permissions after Allow.";
|
|
354
|
+
|
|
355
|
+
/** System Events GUI scripting missing — not a Mail send hang. */
|
|
353
356
|
export const MAIL_GUI_SCRIPTING_GUIDANCE =
|
|
354
357
|
"System Events GUI scripting is unavailable for this process, so the body cannot be typed into Mail. " +
|
|
355
358
|
"mail_send / mail_draft use System Events keystrokes (Tab into the body, typed Returns) so Mail does not cite-wrap the Sent body (FB11734014). " +
|
|
356
|
-
"
|
|
359
|
+
"Allow node → System Events in System Settings → Privacy & Security → Automation (run apple-tools-mcp permissions after install/upgrade) in addition to node → Mail, and keep Accessibility allowed. " +
|
|
360
|
+
SYSTEM_EVENTS_STICKY_DENY_GUIDANCE + " " +
|
|
357
361
|
"This is not a Mail send timeout and not a Mail Automation deny (-1743 / -10004).";
|
|
358
362
|
|
|
359
363
|
/**
|
package/lib/mailWrite.js
CHANGED
|
@@ -160,14 +160,16 @@ export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
|
160
160
|
export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
|
|
161
161
|
export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
|
|
162
162
|
export const SYSTEM_EVENTS_PROBE_MARKER = "ATM_SYSTEM_EVENTS_PROBE";
|
|
163
|
-
/**
|
|
163
|
+
/** Permissions-only marker: process / GUI scripting, not application `name`. */
|
|
164
|
+
export const SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER = "ATM_SYSTEM_EVENTS_AUTOMATION_PROBE";
|
|
165
|
+
/** Compose body AX height vs To/Subject/Cc fields (those are ~22px). Used to reject short header AXTextAreas after Tab; does not walk windows. */
|
|
164
166
|
export const MAIL_BODY_MIN_AX_HEIGHT = 50;
|
|
165
167
|
/** Mini-proven Tab count from default compose focus into the body. */
|
|
166
168
|
export const MAIL_BODY_TAB_MAX = 6;
|
|
167
|
-
/**
|
|
168
|
-
export const MAIL_BODY_CALIBRATE_PROBE = "§";
|
|
169
|
-
/** Cheap System Events Automation probe; SIGKILL if it still hangs. */
|
|
169
|
+
/** Cheap pre-compose System Events check; SIGKILL if it still hangs. Not the permissions grant. */
|
|
170
170
|
export const MAIL_SE_PROBE_TIMEOUT_MS = 4000;
|
|
171
|
+
/** First-run / upgrade permissions probe: long enough for the Allow click. */
|
|
172
|
+
export const SYSTEM_EVENTS_AUTOMATION_PROBE_TIMEOUT_MS = 30000;
|
|
171
173
|
/** Compose osascript budget (make + keystroke + send). Far below the ~60s SE wedge. */
|
|
172
174
|
export const MAIL_COMPOSE_TIMEOUT_MS = 25000;
|
|
173
175
|
export const MAIL_SE_APPLEEVENT_TIMEOUT_SEC = 8;
|
|
@@ -250,6 +252,11 @@ export function isSystemEventsProbeScript(script) {
|
|
|
250
252
|
return String(script || "").includes(SYSTEM_EVENTS_PROBE_MARKER);
|
|
251
253
|
}
|
|
252
254
|
|
|
255
|
+
export function isSystemEventsAutomationProbeScript(script) {
|
|
256
|
+
return String(script || "").includes(SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Cheap pre-compose check. Do not use for `permissions` — `get name` can soft-pass. */
|
|
253
260
|
export function buildSystemEventsProbeScript() {
|
|
254
261
|
const secs = asInteger(MAIL_SE_PROBE_APPLEEVENT_TIMEOUT_SEC, {
|
|
255
262
|
min: 1,
|
|
@@ -268,27 +275,153 @@ if atmUi is false then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
|
268
275
|
return "OK"`;
|
|
269
276
|
}
|
|
270
277
|
|
|
271
|
-
export function probeSystemEventsGui() {
|
|
278
|
+
export function probeSystemEventsGui({ timeout = MAIL_SE_PROBE_TIMEOUT_MS } = {}) {
|
|
272
279
|
return runAppleScript(buildSystemEventsProbeScript(), {
|
|
273
|
-
timeout
|
|
280
|
+
timeout
|
|
274
281
|
});
|
|
275
282
|
}
|
|
276
283
|
|
|
284
|
+
/**
|
|
285
|
+
* First-run / upgrade probe. Soft `tell application "System Events" to get
|
|
286
|
+
* name` is not enough — Mini @2.1.1 reported granted while TCC auth_value=0
|
|
287
|
+
* for node → com.apple.systemevents. Process / GUI scripting (`tell process`)
|
|
288
|
+
* is the grant mail_send actually needs. Does not type into the focused UI.
|
|
289
|
+
* Timeout is long enough for the operator to click Allow. No short inner
|
|
290
|
+
* AppleEvent timeout (same as the Mail compose-and-discard probe).
|
|
291
|
+
*/
|
|
292
|
+
export function buildSystemEventsAutomationProbeScript() {
|
|
293
|
+
return `-- ${SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER}
|
|
294
|
+
tell application "System Events"
|
|
295
|
+
tell process "System Events"
|
|
296
|
+
set atmPid to unix id
|
|
297
|
+
set atmShown to name
|
|
298
|
+
if atmShown is "" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
299
|
+
if atmPid is missing value then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
300
|
+
if (atmPid as integer) is less than or equal to 0 then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
301
|
+
count of UI elements
|
|
302
|
+
end tell
|
|
303
|
+
set atmUi to UI elements enabled
|
|
304
|
+
end tell
|
|
305
|
+
if atmUi is false then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
306
|
+
return "OK"`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* First-run / upgrade probe: a live System Events GUI-scripting Apple Event
|
|
311
|
+
* that creates or verifies Automation → System Events. Does not type.
|
|
312
|
+
* @returns {{ ok: boolean, message: string, kind: string|null }}
|
|
313
|
+
*/
|
|
314
|
+
export function probeSystemEventsAutomation() {
|
|
315
|
+
const action = "system_events_automation_probe";
|
|
316
|
+
const summary = "verify System Events process / GUI scripting (Automation / Apple Events grant; nothing is typed)";
|
|
317
|
+
const result = runAppleScript(buildSystemEventsAutomationProbeScript(), {
|
|
318
|
+
timeout: SYSTEM_EVENTS_AUTOMATION_PROBE_TIMEOUT_MS
|
|
319
|
+
});
|
|
320
|
+
if (!result.ok) {
|
|
321
|
+
const kind = systemEventsGrantKind(result);
|
|
322
|
+
const guidance = isMailAccessibilityDenial(result)
|
|
323
|
+
? MAIL_ACCESSIBILITY_GUIDANCE
|
|
324
|
+
: MAIL_GUI_SCRIPTING_GUIDANCE;
|
|
325
|
+
return {
|
|
326
|
+
ok: false,
|
|
327
|
+
kind,
|
|
328
|
+
message: `${action} failed — attempted to ${summary}. ${guidance}`
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
ok: true,
|
|
333
|
+
kind: null,
|
|
334
|
+
message: writeSuccessMessage(
|
|
335
|
+
action,
|
|
336
|
+
"System Events Automation allowed (process GUI scripting verified; nothing was typed)"
|
|
337
|
+
)
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function systemEventsGrantKind(result) {
|
|
342
|
+
if (!result) return "unknown";
|
|
343
|
+
if (result.kind === "timeout" || result.kind === "tcc" || result.kind === "attribution") {
|
|
344
|
+
return result.kind;
|
|
345
|
+
}
|
|
346
|
+
if (isMailGuiScriptingUnavailable(result) || isMailAccessibilityDenial(result)) {
|
|
347
|
+
return "tcc";
|
|
348
|
+
}
|
|
349
|
+
return result.kind || "unknown";
|
|
350
|
+
}
|
|
351
|
+
|
|
277
352
|
/**
|
|
278
353
|
* AppleScript handlers: Tab into the compose body and type it (typed Returns
|
|
279
354
|
* preserve newlines). Does not walk System Events `windows of process Mail`
|
|
280
|
-
* (macOS 26 reports 0). Calibration
|
|
281
|
-
*
|
|
355
|
+
* (macOS 26 reports 0). Calibration / verify uses the focused AX role (and
|
|
356
|
+
* AX value when it is readable) — never live Mail `content of` the outgoing
|
|
357
|
+
* message. Dual-host Mail 16 / macOS 26: `set content` reads back via
|
|
358
|
+
* `content of`, but Tab + keystroke/paste into the UI body never appears in
|
|
359
|
+
* `content of` (`typedSeen=false`, bodyLen 0) even when typing still produces
|
|
360
|
+
* a plain Sent `.emlx`. Never `set content` / `html content` (FB11734014).
|
|
282
361
|
*
|
|
283
|
-
* Tab count is
|
|
284
|
-
*
|
|
285
|
-
*
|
|
362
|
+
* Tab count is unchanged (MAIL_BODY_TAB_MAX, Mini-proven 6). Fail-closed if
|
|
363
|
+
* AX focus is not a body role after Tab. Cmd-V only if AX value is readable
|
|
364
|
+
* and the needle is still missing. Header checks stay. System Events Apple
|
|
365
|
+
* events are wrapped in `with timeout`.
|
|
286
366
|
*/
|
|
287
367
|
export function buildMailBodyPasteHandler() {
|
|
288
368
|
const tabMax = asInteger(MAIL_BODY_TAB_MAX, { min: 1, max: 12, field: "tabMax" });
|
|
289
369
|
const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
|
|
290
|
-
const
|
|
291
|
-
return
|
|
370
|
+
const bodyMinH = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
|
|
371
|
+
return `${buildAtmFocusedElementHandler()}
|
|
372
|
+
|
|
373
|
+
on atmSafeToPaste()
|
|
374
|
+
tell application "System Events"
|
|
375
|
+
tell process "Mail"
|
|
376
|
+
try
|
|
377
|
+
set fe to my atmFocusedElement()
|
|
378
|
+
if fe is missing value then return false
|
|
379
|
+
set r to (role of fe) as string
|
|
380
|
+
if r is "AXWindow" then return false
|
|
381
|
+
if r is "AXTextField" then return false
|
|
382
|
+
if r is "AXComboBox" then return false
|
|
383
|
+
if r is "AXButton" then return false
|
|
384
|
+
if r is "AXMenuButton" then return false
|
|
385
|
+
if r is "AXPopUpButton" then return false
|
|
386
|
+
set atmH to 0
|
|
387
|
+
try
|
|
388
|
+
set {atmW, atmH} to size of fe
|
|
389
|
+
end try
|
|
390
|
+
if r is "AXTextArea" and atmH > 0 and atmH < ${bodyMinH} then return false
|
|
391
|
+
ignoring case
|
|
392
|
+
set bits to ""
|
|
393
|
+
try
|
|
394
|
+
set bits to bits & (description of fe as string) & " "
|
|
395
|
+
end try
|
|
396
|
+
try
|
|
397
|
+
set bits to bits & (name of fe as string) & " "
|
|
398
|
+
end try
|
|
399
|
+
if bits contains "To:" then return false
|
|
400
|
+
if bits contains "Cc:" then return false
|
|
401
|
+
if bits contains "Bcc:" then return false
|
|
402
|
+
if bits contains "Subject" then return false
|
|
403
|
+
end ignoring
|
|
404
|
+
if r is "AXWebArea" then return true
|
|
405
|
+
if r is "AXTextArea" then return true
|
|
406
|
+
if atmH >= ${bodyMinH} then return true
|
|
407
|
+
end try
|
|
408
|
+
return false
|
|
409
|
+
end tell
|
|
410
|
+
end tell
|
|
411
|
+
end atmSafeToPaste
|
|
412
|
+
|
|
413
|
+
on atmAxFocusedValue()
|
|
414
|
+
tell application "System Events"
|
|
415
|
+
try
|
|
416
|
+
set fe to my atmFocusedElement()
|
|
417
|
+
if fe is missing value then return ""
|
|
418
|
+
return value of fe as string
|
|
419
|
+
end try
|
|
420
|
+
end tell
|
|
421
|
+
return ""
|
|
422
|
+
end atmAxFocusedValue
|
|
423
|
+
|
|
424
|
+
on atmSplitParagraphs(bodyText)
|
|
292
425
|
set atmSavedDelim to AppleScript's text item delimiters
|
|
293
426
|
set AppleScript's text item delimiters to return
|
|
294
427
|
set atmCrParts to text items of bodyText
|
|
@@ -326,16 +459,6 @@ on atmKeystrokeText(atmText)
|
|
|
326
459
|
end tell
|
|
327
460
|
end atmKeystrokeText
|
|
328
461
|
|
|
329
|
-
on atmMailBodyContains(msg, needle)
|
|
330
|
-
tell application "Mail"
|
|
331
|
-
try
|
|
332
|
-
set atmGot to content of msg as string
|
|
333
|
-
if atmGot contains needle then return true
|
|
334
|
-
end try
|
|
335
|
-
end tell
|
|
336
|
-
return false
|
|
337
|
-
end atmMailBodyContains
|
|
338
|
-
|
|
339
462
|
on atmTabIntoMailBody(msg)
|
|
340
463
|
tell application "Mail" to activate
|
|
341
464
|
delay 0.25
|
|
@@ -346,23 +469,7 @@ on atmTabIntoMailBody(msg)
|
|
|
346
469
|
end timeout
|
|
347
470
|
delay 0.08
|
|
348
471
|
repeat with atmN from 0 to ${tabMax}
|
|
349
|
-
|
|
350
|
-
tell application "System Events"
|
|
351
|
-
tell process "Mail"
|
|
352
|
-
keystroke ${probe}
|
|
353
|
-
end tell
|
|
354
|
-
end tell
|
|
355
|
-
end timeout
|
|
356
|
-
delay 0.08
|
|
357
|
-
set atmHit to my atmMailBodyContains(msg, ${probe})
|
|
358
|
-
with timeout of ${seSecs} seconds
|
|
359
|
-
tell application "System Events"
|
|
360
|
-
tell process "Mail"
|
|
361
|
-
key code 51
|
|
362
|
-
end tell
|
|
363
|
-
end tell
|
|
364
|
-
end timeout
|
|
365
|
-
if atmHit then return
|
|
472
|
+
if my atmSafeToPaste() then return
|
|
366
473
|
if atmN is ${tabMax} then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
367
474
|
with timeout of ${seSecs} seconds
|
|
368
475
|
tell application "System Events"
|
|
@@ -423,11 +530,15 @@ end atmPasteMailBodyFallback
|
|
|
423
530
|
on atmFillMailBody(bodyText, needle, msg)
|
|
424
531
|
try
|
|
425
532
|
atmTabIntoMailBody(msg)
|
|
533
|
+
if my atmSafeToPaste() is false then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
426
534
|
atmTypeMailBody(bodyText)
|
|
427
535
|
delay 0.12
|
|
428
536
|
if needle is not "" then
|
|
429
|
-
|
|
430
|
-
|
|
537
|
+
set atmAxVal to my atmAxFocusedValue()
|
|
538
|
+
if atmAxVal is not "" then
|
|
539
|
+
if atmAxVal does not contain needle then
|
|
540
|
+
atmPasteMailBodyFallback(bodyText)
|
|
541
|
+
end if
|
|
431
542
|
end if
|
|
432
543
|
end if
|
|
433
544
|
on error errMsg number errNum
|
|
@@ -457,20 +568,10 @@ function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
|
457
568
|
})
|
|
458
569
|
.join("\n");
|
|
459
570
|
const bodyCheck = needle
|
|
460
|
-
? ` set
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
end
|
|
464
|
-
if atmGotBody is "" then
|
|
465
|
-
try
|
|
466
|
-
save newMessage
|
|
467
|
-
end try
|
|
468
|
-
delay 0.2
|
|
469
|
-
try
|
|
470
|
-
set atmGotBody to content of newMessage as string
|
|
471
|
-
end try
|
|
472
|
-
end if
|
|
473
|
-
if atmGotBody does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`
|
|
571
|
+
? ` set atmAxVal to my atmAxFocusedValue()
|
|
572
|
+
if atmAxVal is not "" then
|
|
573
|
+
if atmAxVal does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
574
|
+
end if`
|
|
474
575
|
: "";
|
|
475
576
|
return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
476
577
|
if (count of cc recipients of newMessage) is not ${ccCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
@@ -490,10 +591,13 @@ ${bodyCheck}`;
|
|
|
490
591
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
491
592
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
492
593
|
* Recipients and subject are AppleScript properties (never paste). The body
|
|
493
|
-
* is typed into the native compose editor via System Events (Tab into body
|
|
494
|
-
* typed Returns for newlines, Cmd-V
|
|
495
|
-
*
|
|
496
|
-
*
|
|
594
|
+
* is typed into the native compose editor via System Events (Tab into body
|
|
595
|
+
* until AX focused role is a body field, typed Returns for newlines, Cmd-V
|
|
596
|
+
* only if AX value is readable and the needle is still missing). Do not walk
|
|
597
|
+
* `windows of process Mail`. Do not use Mail `content of` as the oracle that
|
|
598
|
+
* typed/pasted text landed (Mail 16 / macOS 26: that property stays empty).
|
|
599
|
+
* If AX body focus cannot be established, or headers change, abort + delete
|
|
600
|
+
* (nothing sent). Ship prove that the body landed is Sent `.emlx` after send.
|
|
497
601
|
*/
|
|
498
602
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
499
603
|
const recipients = [
|
package/lib/permissions.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* First-run / upgrade permissions command.
|
|
3
3
|
*
|
|
4
|
-
* Probes Contacts, Calendar, Mail, and
|
|
5
|
-
* macOS can pop Allow dialogs for that node binary.
|
|
6
|
-
* this command cannot grant silently.
|
|
4
|
+
* Probes Contacts, Calendar, Mail, Messages, and System Events under
|
|
5
|
+
* process.execPath so macOS can pop Allow dialogs for that node binary.
|
|
6
|
+
* The user clicks Allow; this command cannot grant silently.
|
|
7
7
|
*
|
|
8
8
|
* Always runs in this process (never via the write bridge): the point is to
|
|
9
9
|
* attach the dialogs to process.execPath. If the invoked CLI / shebang
|
|
10
10
|
* points at a different node, print a WARN — Allows attach to execPath.
|
|
11
11
|
* Mail and Messages use the existing live Apple Events helpers — dry_run of
|
|
12
12
|
* mail_send / messages_send never talks to those apps and does not count.
|
|
13
|
+
* System Events uses process / GUI scripting (`tell process`), not a soft
|
|
14
|
+
* application `get name` — that can succeed without AppleEvents auth.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import fs from "fs";
|
|
16
18
|
import path from "path";
|
|
17
|
-
import { classifyAppleScriptError } from "./appleScript.js";
|
|
18
|
-
import { probeMailAutomation } from "./mailWrite.js";
|
|
19
|
+
import { classifyAppleScriptError, SYSTEM_EVENTS_STICKY_DENY_GUIDANCE } from "./appleScript.js";
|
|
20
|
+
import { probeMailAutomation, probeSystemEventsAutomation } from "./mailWrite.js";
|
|
19
21
|
import { probeMessagesAutomation } from "./messagesWrite.js";
|
|
20
22
|
import { probeContactsAutomation } from "./contactsWrite.js";
|
|
21
23
|
import { probeCalendarAutomation } from "./calendarWrite.js";
|
|
22
24
|
import { detectLaunchAgentContext, hostAutomationAdvice } from "./writeRouting.js";
|
|
23
25
|
|
|
24
|
-
export const REQUIRED_SURFACES = ["Contacts", "Calendar", "Mail", "Messages"];
|
|
26
|
+
export const REQUIRED_SURFACES = ["Contacts", "Calendar", "Mail", "Messages", "System Events"];
|
|
25
27
|
|
|
26
28
|
/** Mini ship-gate host example — not a universal path. */
|
|
27
29
|
export const EXAMPLE_MINI_NODE = "/Users/petercoates/.local/node/bin/node";
|
|
@@ -172,6 +174,9 @@ export function classifyGrantStatus(result) {
|
|
|
172
174
|
if (kind === "app_not_running") {
|
|
173
175
|
return GRANT_ERROR;
|
|
174
176
|
}
|
|
177
|
+
if (/accessibility_denied|assistive access|gui_scripting_unavailable/i.test(text)) {
|
|
178
|
+
return GRANT_MISSING;
|
|
179
|
+
}
|
|
175
180
|
if (/tcc|automation deny|not authorized|not permitted|not allowed|timed out|etimedout|responsible-process|attribution/i.test(text)) {
|
|
176
181
|
return GRANT_MISSING;
|
|
177
182
|
}
|
|
@@ -198,7 +203,7 @@ export function exitCodeForGrants(grants) {
|
|
|
198
203
|
|
|
199
204
|
/**
|
|
200
205
|
* Advisory Full Disk Access touch. Read tools need FDA on this node; it is
|
|
201
|
-
* not one of the
|
|
206
|
+
* not one of the required Automation grants and never fails the command.
|
|
202
207
|
*
|
|
203
208
|
* @returns {{ status: "readable"|"missing"|"skipped", message: string }}
|
|
204
209
|
*/
|
|
@@ -247,7 +252,8 @@ export function defaultPermissionsProbes() {
|
|
|
247
252
|
Contacts: probeContactsAutomation,
|
|
248
253
|
Calendar: probeCalendarAutomation,
|
|
249
254
|
Mail: probeMailAutomation,
|
|
250
|
-
Messages: probeMessagesAutomation
|
|
255
|
+
Messages: probeMessagesAutomation,
|
|
256
|
+
"System Events": probeSystemEventsAutomation
|
|
251
257
|
};
|
|
252
258
|
}
|
|
253
259
|
|
|
@@ -267,12 +273,15 @@ function bannerLines(binary, version) {
|
|
|
267
273
|
"dry_run of mail_send / messages_send does not count: this command uses real Apple Events.",
|
|
268
274
|
"Nothing is sent to third parties. Mail composes and discards a temporary outgoing message.",
|
|
269
275
|
"Messages only enumerates accounts. Contacts creates and deletes a throwaway person in-script.",
|
|
270
|
-
"Calendar lists calendars only — no events are created."
|
|
276
|
+
"Calendar lists calendars only — no events are created.",
|
|
277
|
+
"System Events is probed with process / GUI scripting (tell process + unix id; nothing is typed).",
|
|
278
|
+
"A soft tell System Events to get name is not enough — that can succeed without AppleEvents auth.",
|
|
279
|
+
"A prior Don't Allow is a sticky Deny: turn System Events on for this node in Automation, then re-run this command."
|
|
271
280
|
];
|
|
272
281
|
}
|
|
273
282
|
|
|
274
283
|
/**
|
|
275
|
-
* Run the
|
|
284
|
+
* Run the live Automation probes and print a grant report.
|
|
276
285
|
*
|
|
277
286
|
* @param {object} [options]
|
|
278
287
|
* @param {string} [options.execPath]
|
|
@@ -349,11 +358,14 @@ export async function runPermissionsCommand({
|
|
|
349
358
|
|
|
350
359
|
const code = exitCodeForGrants(grants);
|
|
351
360
|
if (code === 0) {
|
|
352
|
-
log("Result: PASS — Contacts, Calendar, Mail, and
|
|
361
|
+
log("Result: PASS — Contacts, Calendar, Mail, Messages, and System Events are granted for this node.");
|
|
353
362
|
log("Safe to re-run; already-granted surfaces report OK without another click.");
|
|
354
363
|
} else {
|
|
355
364
|
log("Result: INCOMPLETE — one or more required grants are missing or errored.");
|
|
356
365
|
log(hostAdvice);
|
|
366
|
+
if (grants["System Events"] !== GRANT_GRANTED) {
|
|
367
|
+
log(SYSTEM_EVENTS_STICKY_DENY_GUIDANCE);
|
|
368
|
+
}
|
|
357
369
|
log("Click Allow for the missing surfaces and re-run this command. Exit is non-zero (fail closed).");
|
|
358
370
|
}
|
|
359
371
|
return code;
|
package/lib/writeRouting.js
CHANGED
|
@@ -91,7 +91,7 @@ export function terminalAutomationAdvice(execPath = process.execPath) {
|
|
|
91
91
|
return (
|
|
92
92
|
`Run this from Terminal.app (short-lived CLI / MacBook host — no always-on indexer). ` +
|
|
93
93
|
`Click Allow for the printed process.execPath${printed}. ` +
|
|
94
|
-
`Check System Settings → Privacy & Security → Automation for that node → Contacts, Calendar, Mail, and
|
|
94
|
+
`Check System Settings → Privacy & Security → Automation for that node → Contacts, Calendar, Mail, Messages, and System Events. ` +
|
|
95
95
|
`Do not start apple-tools-indexer.`
|
|
96
96
|
);
|
|
97
97
|
}
|