apple-tools-mcp 2.1.0 → 2.1.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.
- package/README.md +12 -10
- package/lib/appleScript.js +1 -1
- package/lib/mailWrite.js +131 -60
- package/lib/permissions.js +15 -10
- 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 asked for its process name (nothing is typed) so the keystroke compose path can pop **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**. 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
|
@@ -353,7 +353,7 @@ export const MAIL_ACCESSIBILITY_GUIDANCE =
|
|
|
353
353
|
export const MAIL_GUI_SCRIPTING_GUIDANCE =
|
|
354
354
|
"System Events GUI scripting is unavailable for this process, so the body cannot be typed into Mail. " +
|
|
355
355
|
"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
|
-
"
|
|
356
|
+
"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. " +
|
|
357
357
|
"This is not a Mail send timeout and not a Mail Automation deny (-1743 / -10004).";
|
|
358
358
|
|
|
359
359
|
/**
|
package/lib/mailWrite.js
CHANGED
|
@@ -160,14 +160,14 @@ 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
|
-
/** Compose body AX height vs To/Subject/Cc fields (those are ~22px).
|
|
163
|
+
/** 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
164
|
export const MAIL_BODY_MIN_AX_HEIGHT = 50;
|
|
165
165
|
/** Mini-proven Tab count from default compose focus into the body. */
|
|
166
166
|
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. */
|
|
167
|
+
/** Cheap System Events Automation probe used before compose; SIGKILL if it still hangs. */
|
|
170
168
|
export const MAIL_SE_PROBE_TIMEOUT_MS = 4000;
|
|
169
|
+
/** First-run / upgrade permissions probe: long enough for the Allow click. */
|
|
170
|
+
export const SYSTEM_EVENTS_AUTOMATION_PROBE_TIMEOUT_MS = 30000;
|
|
171
171
|
/** Compose osascript budget (make + keystroke + send). Far below the ~60s SE wedge. */
|
|
172
172
|
export const MAIL_COMPOSE_TIMEOUT_MS = 25000;
|
|
173
173
|
export const MAIL_SE_APPLEEVENT_TIMEOUT_SEC = 8;
|
|
@@ -268,27 +268,127 @@ if atmUi is false then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
|
268
268
|
return "OK"`;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
export function probeSystemEventsGui() {
|
|
271
|
+
export function probeSystemEventsGui({ timeout = MAIL_SE_PROBE_TIMEOUT_MS } = {}) {
|
|
272
272
|
return runAppleScript(buildSystemEventsProbeScript(), {
|
|
273
|
-
timeout
|
|
273
|
+
timeout
|
|
274
274
|
});
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* First-run / upgrade probe: a live System Events Apple Event that pops
|
|
279
|
+
* Automation → System Events. Does not type into the focused UI.
|
|
280
|
+
* Timeout is long enough for the operator to click Allow.
|
|
281
|
+
* @returns {{ ok: boolean, message: string, kind: string|null }}
|
|
282
|
+
*/
|
|
283
|
+
export function probeSystemEventsAutomation() {
|
|
284
|
+
const action = "system_events_automation_probe";
|
|
285
|
+
const summary = "ask System Events for its name (Automation / Apple Events check; nothing is typed)";
|
|
286
|
+
const result = probeSystemEventsGui({ timeout: SYSTEM_EVENTS_AUTOMATION_PROBE_TIMEOUT_MS });
|
|
287
|
+
if (!result.ok) {
|
|
288
|
+
const kind = systemEventsGrantKind(result);
|
|
289
|
+
const guidance = isMailAccessibilityDenial(result)
|
|
290
|
+
? MAIL_ACCESSIBILITY_GUIDANCE
|
|
291
|
+
: MAIL_GUI_SCRIPTING_GUIDANCE;
|
|
292
|
+
return {
|
|
293
|
+
ok: false,
|
|
294
|
+
kind,
|
|
295
|
+
message: `${action} failed — attempted to ${summary}. ${guidance}`
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
ok: true,
|
|
300
|
+
kind: null,
|
|
301
|
+
message: writeSuccessMessage(
|
|
302
|
+
action,
|
|
303
|
+
"System Events Automation allowed (process name read; nothing was typed)"
|
|
304
|
+
)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function systemEventsGrantKind(result) {
|
|
309
|
+
if (!result) return "unknown";
|
|
310
|
+
if (result.kind === "timeout" || result.kind === "tcc" || result.kind === "attribution") {
|
|
311
|
+
return result.kind;
|
|
312
|
+
}
|
|
313
|
+
if (isMailGuiScriptingUnavailable(result) || isMailAccessibilityDenial(result)) {
|
|
314
|
+
return "tcc";
|
|
315
|
+
}
|
|
316
|
+
return result.kind || "unknown";
|
|
317
|
+
}
|
|
318
|
+
|
|
277
319
|
/**
|
|
278
320
|
* AppleScript handlers: Tab into the compose body and type it (typed Returns
|
|
279
321
|
* preserve newlines). Does not walk System Events `windows of process Mail`
|
|
280
|
-
* (macOS 26 reports 0). Calibration
|
|
281
|
-
*
|
|
322
|
+
* (macOS 26 reports 0). Calibration / verify uses the focused AX role (and
|
|
323
|
+
* AX value when it is readable) — never live Mail `content of` the outgoing
|
|
324
|
+
* message. Dual-host Mail 16 / macOS 26: `set content` reads back via
|
|
325
|
+
* `content of`, but Tab + keystroke/paste into the UI body never appears in
|
|
326
|
+
* `content of` (`typedSeen=false`, bodyLen 0) even when typing still produces
|
|
327
|
+
* a plain Sent `.emlx`. Never `set content` / `html content` (FB11734014).
|
|
282
328
|
*
|
|
283
|
-
* Tab count is
|
|
284
|
-
*
|
|
285
|
-
*
|
|
329
|
+
* Tab count is unchanged (MAIL_BODY_TAB_MAX, Mini-proven 6). Fail-closed if
|
|
330
|
+
* AX focus is not a body role after Tab. Cmd-V only if AX value is readable
|
|
331
|
+
* and the needle is still missing. Header checks stay. System Events Apple
|
|
332
|
+
* events are wrapped in `with timeout`.
|
|
286
333
|
*/
|
|
287
334
|
export function buildMailBodyPasteHandler() {
|
|
288
335
|
const tabMax = asInteger(MAIL_BODY_TAB_MAX, { min: 1, max: 12, field: "tabMax" });
|
|
289
336
|
const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
|
|
290
|
-
const
|
|
291
|
-
return
|
|
337
|
+
const bodyMinH = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
|
|
338
|
+
return `${buildAtmFocusedElementHandler()}
|
|
339
|
+
|
|
340
|
+
on atmSafeToPaste()
|
|
341
|
+
tell application "System Events"
|
|
342
|
+
tell process "Mail"
|
|
343
|
+
try
|
|
344
|
+
set fe to my atmFocusedElement()
|
|
345
|
+
if fe is missing value then return false
|
|
346
|
+
set r to (role of fe) as string
|
|
347
|
+
if r is "AXWindow" then return false
|
|
348
|
+
if r is "AXTextField" then return false
|
|
349
|
+
if r is "AXComboBox" then return false
|
|
350
|
+
if r is "AXButton" then return false
|
|
351
|
+
if r is "AXMenuButton" then return false
|
|
352
|
+
if r is "AXPopUpButton" then return false
|
|
353
|
+
set atmH to 0
|
|
354
|
+
try
|
|
355
|
+
set {atmW, atmH} to size of fe
|
|
356
|
+
end try
|
|
357
|
+
if r is "AXTextArea" and atmH > 0 and atmH < ${bodyMinH} then return false
|
|
358
|
+
ignoring case
|
|
359
|
+
set bits to ""
|
|
360
|
+
try
|
|
361
|
+
set bits to bits & (description of fe as string) & " "
|
|
362
|
+
end try
|
|
363
|
+
try
|
|
364
|
+
set bits to bits & (name of fe as string) & " "
|
|
365
|
+
end try
|
|
366
|
+
if bits contains "To:" then return false
|
|
367
|
+
if bits contains "Cc:" then return false
|
|
368
|
+
if bits contains "Bcc:" then return false
|
|
369
|
+
if bits contains "Subject" then return false
|
|
370
|
+
end ignoring
|
|
371
|
+
if r is "AXWebArea" then return true
|
|
372
|
+
if r is "AXTextArea" then return true
|
|
373
|
+
if atmH >= ${bodyMinH} then return true
|
|
374
|
+
end try
|
|
375
|
+
return false
|
|
376
|
+
end tell
|
|
377
|
+
end tell
|
|
378
|
+
end atmSafeToPaste
|
|
379
|
+
|
|
380
|
+
on atmAxFocusedValue()
|
|
381
|
+
tell application "System Events"
|
|
382
|
+
try
|
|
383
|
+
set fe to my atmFocusedElement()
|
|
384
|
+
if fe is missing value then return ""
|
|
385
|
+
return value of fe as string
|
|
386
|
+
end try
|
|
387
|
+
end tell
|
|
388
|
+
return ""
|
|
389
|
+
end atmAxFocusedValue
|
|
390
|
+
|
|
391
|
+
on atmSplitParagraphs(bodyText)
|
|
292
392
|
set atmSavedDelim to AppleScript's text item delimiters
|
|
293
393
|
set AppleScript's text item delimiters to return
|
|
294
394
|
set atmCrParts to text items of bodyText
|
|
@@ -326,16 +426,6 @@ on atmKeystrokeText(atmText)
|
|
|
326
426
|
end tell
|
|
327
427
|
end atmKeystrokeText
|
|
328
428
|
|
|
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
429
|
on atmTabIntoMailBody(msg)
|
|
340
430
|
tell application "Mail" to activate
|
|
341
431
|
delay 0.25
|
|
@@ -346,23 +436,7 @@ on atmTabIntoMailBody(msg)
|
|
|
346
436
|
end timeout
|
|
347
437
|
delay 0.08
|
|
348
438
|
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
|
|
439
|
+
if my atmSafeToPaste() then return
|
|
366
440
|
if atmN is ${tabMax} then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
367
441
|
with timeout of ${seSecs} seconds
|
|
368
442
|
tell application "System Events"
|
|
@@ -423,11 +497,15 @@ end atmPasteMailBodyFallback
|
|
|
423
497
|
on atmFillMailBody(bodyText, needle, msg)
|
|
424
498
|
try
|
|
425
499
|
atmTabIntoMailBody(msg)
|
|
500
|
+
if my atmSafeToPaste() is false then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
426
501
|
atmTypeMailBody(bodyText)
|
|
427
502
|
delay 0.12
|
|
428
503
|
if needle is not "" then
|
|
429
|
-
|
|
430
|
-
|
|
504
|
+
set atmAxVal to my atmAxFocusedValue()
|
|
505
|
+
if atmAxVal is not "" then
|
|
506
|
+
if atmAxVal does not contain needle then
|
|
507
|
+
atmPasteMailBodyFallback(bodyText)
|
|
508
|
+
end if
|
|
431
509
|
end if
|
|
432
510
|
end if
|
|
433
511
|
on error errMsg number errNum
|
|
@@ -457,20 +535,10 @@ function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
|
457
535
|
})
|
|
458
536
|
.join("\n");
|
|
459
537
|
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}"`
|
|
538
|
+
? ` set atmAxVal to my atmAxFocusedValue()
|
|
539
|
+
if atmAxVal is not "" then
|
|
540
|
+
if atmAxVal does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
541
|
+
end if`
|
|
474
542
|
: "";
|
|
475
543
|
return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
476
544
|
if (count of cc recipients of newMessage) is not ${ccCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
@@ -490,10 +558,13 @@ ${bodyCheck}`;
|
|
|
490
558
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
491
559
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
492
560
|
* 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
|
-
*
|
|
561
|
+
* is typed into the native compose editor via System Events (Tab into body
|
|
562
|
+
* until AX focused role is a body field, typed Returns for newlines, Cmd-V
|
|
563
|
+
* only if AX value is readable and the needle is still missing). Do not walk
|
|
564
|
+
* `windows of process Mail`. Do not use Mail `content of` as the oracle that
|
|
565
|
+
* typed/pasted text landed (Mail 16 / macOS 26: that property stays empty).
|
|
566
|
+
* If AX body focus cannot be established, or headers change, abort + delete
|
|
567
|
+
* (nothing sent). Ship prove that the body landed is Sent `.emlx` after send.
|
|
497
568
|
*/
|
|
498
569
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
499
570
|
const recipients = [
|
package/lib/permissions.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
import fs from "fs";
|
|
16
16
|
import path from "path";
|
|
17
17
|
import { classifyAppleScriptError } from "./appleScript.js";
|
|
18
|
-
import { probeMailAutomation } from "./mailWrite.js";
|
|
18
|
+
import { probeMailAutomation, probeSystemEventsAutomation } from "./mailWrite.js";
|
|
19
19
|
import { probeMessagesAutomation } from "./messagesWrite.js";
|
|
20
20
|
import { probeContactsAutomation } from "./contactsWrite.js";
|
|
21
21
|
import { probeCalendarAutomation } from "./calendarWrite.js";
|
|
22
22
|
import { detectLaunchAgentContext, hostAutomationAdvice } from "./writeRouting.js";
|
|
23
23
|
|
|
24
|
-
export const REQUIRED_SURFACES = ["Contacts", "Calendar", "Mail", "Messages"];
|
|
24
|
+
export const REQUIRED_SURFACES = ["Contacts", "Calendar", "Mail", "Messages", "System Events"];
|
|
25
25
|
|
|
26
26
|
/** Mini ship-gate host example — not a universal path. */
|
|
27
27
|
export const EXAMPLE_MINI_NODE = "/Users/petercoates/.local/node/bin/node";
|
|
@@ -172,6 +172,9 @@ export function classifyGrantStatus(result) {
|
|
|
172
172
|
if (kind === "app_not_running") {
|
|
173
173
|
return GRANT_ERROR;
|
|
174
174
|
}
|
|
175
|
+
if (/accessibility_denied|assistive access|gui_scripting_unavailable/i.test(text)) {
|
|
176
|
+
return GRANT_MISSING;
|
|
177
|
+
}
|
|
175
178
|
if (/tcc|automation deny|not authorized|not permitted|not allowed|timed out|etimedout|responsible-process|attribution/i.test(text)) {
|
|
176
179
|
return GRANT_MISSING;
|
|
177
180
|
}
|
|
@@ -198,7 +201,7 @@ export function exitCodeForGrants(grants) {
|
|
|
198
201
|
|
|
199
202
|
/**
|
|
200
203
|
* Advisory Full Disk Access touch. Read tools need FDA on this node; it is
|
|
201
|
-
* not one of the
|
|
204
|
+
* not one of the required Automation grants and never fails the command.
|
|
202
205
|
*
|
|
203
206
|
* @returns {{ status: "readable"|"missing"|"skipped", message: string }}
|
|
204
207
|
*/
|
|
@@ -247,7 +250,8 @@ export function defaultPermissionsProbes() {
|
|
|
247
250
|
Contacts: probeContactsAutomation,
|
|
248
251
|
Calendar: probeCalendarAutomation,
|
|
249
252
|
Mail: probeMailAutomation,
|
|
250
|
-
Messages: probeMessagesAutomation
|
|
253
|
+
Messages: probeMessagesAutomation,
|
|
254
|
+
"System Events": probeSystemEventsAutomation
|
|
251
255
|
};
|
|
252
256
|
}
|
|
253
257
|
|
|
@@ -267,12 +271,13 @@ function bannerLines(binary, version) {
|
|
|
267
271
|
"dry_run of mail_send / messages_send does not count: this command uses real Apple Events.",
|
|
268
272
|
"Nothing is sent to third parties. Mail composes and discards a temporary outgoing message.",
|
|
269
273
|
"Messages only enumerates accounts. Contacts creates and deletes a throwaway person in-script.",
|
|
270
|
-
"Calendar lists calendars only — no events are created."
|
|
274
|
+
"Calendar lists calendars only — no events are created.",
|
|
275
|
+
"System Events is asked for its process name (mail_send keystrokes; nothing is typed)."
|
|
271
276
|
];
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
/**
|
|
275
|
-
* Run the
|
|
280
|
+
* Run the live Automation probes and print a grant report.
|
|
276
281
|
*
|
|
277
282
|
* @param {object} [options]
|
|
278
283
|
* @param {string} [options.execPath]
|
|
@@ -349,7 +354,7 @@ export async function runPermissionsCommand({
|
|
|
349
354
|
|
|
350
355
|
const code = exitCodeForGrants(grants);
|
|
351
356
|
if (code === 0) {
|
|
352
|
-
log("Result: PASS — Contacts, Calendar, Mail, and
|
|
357
|
+
log("Result: PASS — Contacts, Calendar, Mail, Messages, and System Events are granted for this node.");
|
|
353
358
|
log("Safe to re-run; already-granted surfaces report OK without another click.");
|
|
354
359
|
} else {
|
|
355
360
|
log("Result: INCOMPLETE — one or more required grants are missing or errored.");
|
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
|
}
|