apple-tools-mcp 2.0.10 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -122,7 +122,7 @@ This is the first-run flow for Mail, Messages, Contacts, and Calendar **writes**
122
122
  ```
123
123
 
124
124
  That is the ship-gate context. An embedded agent shell, IDE terminal, or MCP host app subprocess is **not** the ship-gate host unless the write bridge is up and the work executes inside launchd-owned `node`.
125
- 3. When prompts appear, click **Allow** for **`node`** to control **Mail**, **Messages**, **Contacts**, and **Calendar**. Watch the host — Allow **`node`** (the LaunchAgent binary), not any other app. Use whatever path the LaunchAgent plist / `which node` reports — `/Users/petercoates/.local/node/bin/node` is a Mini *example* only, not a universal path. Do **not** approve the MCP client / host app that launched a short-lived stdio server.
125
+ 3. When prompts appear, click **Allow** for **`node`** to control **Mail**, **Messages**, **Contacts**, **Calendar**, and **System Events** (needed for `mail_send` / `mail_draft` keystrokes under the LaunchAgent). Watch the host — Allow **`node`** (the LaunchAgent binary), not any other app. Use whatever path the LaunchAgent plist / `which node` reports — `/Users/petercoates/.local/node/bin/node` is a Mini *example* only, not a universal path. Do **not** approve the MCP client / host app that launched a short-lived stdio server.
126
126
  4. **Keep Contacts, Mail, and Messages running** on the write host (leave the apps open; do not quit them). Cold `tell application "Contacts"` under launchd often fails with `-600` / “application isn't running” instead of auto-launching — that is **not** an Automation deny. The write path launches Contacts.app before CRUD, but write reliability still requires those three apps to stay running. **Calendar does not need to stay open** (Calendar writes go through EventKit).
127
127
  5. **Full Disk Access** on `node` is a separate grant and covers **reads** (Mail / Messages / Calendar / AddressBook databases). Write tools need the Automation / Apple Events grants to Mail.app (`mail_send`, `mail_draft`, `mail_reply`, `mail_forward`, and the other mail writes), Messages.app (`messages_send`), Contacts.app, and Calendar.app.
128
128
  6. npm Trusted Publisher / publish tokens are unrelated to TCC. Do not confuse them with this setup.
@@ -308,7 +308,7 @@ Missing `config.json` is fine — env then the 5-minute default apply.
308
308
 
309
309
  On Mini, run the **indexer daemon**, not a sleep-pipe wrapper around `apple-tools-mcp`. Claude Desktop and other clients still attach via short-lived stdio MCP (`npx -y apple-tools-mcp` or the global `apple-tools-mcp` bin).
310
310
 
311
- The daemon does two jobs: it refreshes the vector index, and it serves the **write bridge** at `~/.apple-tools-mcp/writer.sock` so stdio clients can perform Mail / Messages / Contacts / Calendar writes that their host app cannot be granted (see [step 2b](#2b-grant-automation-for-write-tools-first-run--ship-gate)). When macOS prompts, Allow **`node`** (the LaunchAgent binary) to control Mail.app, Messages.app, Contacts.app, and Calendar.app. 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.
311
+ The daemon does two jobs: it refreshes the vector index, and it serves the **write bridge** at `~/.apple-tools-mcp/writer.sock` so stdio clients can perform Mail / Messages / Contacts / Calendar writes that their host app cannot be granted (see [step 2b](#2b-grant-automation-for-write-tools-first-run--ship-gate)). When macOS prompts, Allow **`node`** (the LaunchAgent binary) to control Mail.app, Messages.app, Contacts.app, Calendar.app, **and System Events** (mail_send / mail_draft type the body via keystrokes). 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.
312
312
 
313
313
  **Keep Contacts.app, Mail.app, and Messages.app running all the time** on the Mini (and on any MacBook that does local writes). Quitting them makes Apple Events to those apps unreliable (`-600` / “application isn't running”), which is a cold-launch miss — not a missing Automation grant. The Contacts write path launches Contacts.app before add/edit/remove, but do not rely on that instead of leaving the apps open. **Calendar.app does not need to stay open**; Calendar writes use EventKit.
314
314
 
@@ -464,19 +464,21 @@ Two arguments are available on **every** write tool:
464
464
 
465
465
  `body_format: "html"` uses the same compose path with a tag-stripped body pasted into Mail's native editor. It does **not** set Mail's `content` or `html content` — those setters cite-wrap the whole message (`>` prefixes and `<blockquote type="cite">`, same iOS purple bar). Mail may still generate its own HTML alternative from the native compose. The default is plain text.
466
466
 
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 paste the body into the compose window (System Events). 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.
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
- Paste uses System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) in addition to Automation → Mail. That Accessibility deny is not a Mail Automation deny (`-1743` / `-10004`). Body focus uses `text area` / `scroll area` / `text field` and `UI element whose role is "AXWebArea"` — never the System Events class `web area`, which does not compile on macOS 26.x (**-2741**, “Expected class name but found identifier”). The pre-paste focus read uses `first UI element whose focused is true` and `value of attribute "AXFocusedUIElement"` — never the compound `focused UI element`, which does not compile on System Events 26.x (**-2741** on MacBook 26.1, **-2740** on Mini 26.6.2).
469
+ Keystrokes use System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) **and** Automation → Mail. On Mini, the LaunchAgent's node also needs **Automation → System Events** if writes run through the write bridge. An Accessibility / System Events deny is not a Mail Automation deny (`-1743` / `-10004`).
470
470
 
471
- **Compose body focus (2.0.8).** After `make new outgoing message`, To and Subject are AppleScript properties (never paste). Cmd-V runs only after the caret leaves the header: a tall `AXWebArea` (HTML compose body), a tall `text area` (plain compose; short To/Subject fields are skipped), Subject then Tab, or a click in the lower two-thirds of the compose window. If focus is still a header field, the tool raises `BODY_FOCUS_FAILED` and does **not** paste. After paste it checks To/Cc/Bcc counts, the exact subject, and that the native body contains the intended text. A mismatch is `BODY_PASTE_MISDIRECTED`: the outgoing message is deleted and nothing is sent. Quote-prefix Sent prove and Sent/Outbox verify stay gated on this paste-focus contract — 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`).
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
 
473
- **macOS 26 focus compile (2.0.9).** Dual-host Sent prove on `@2.0.8` never reached paste: `atmSafeToPaste` used bare `focused UI element`, which System Events 26.x rejects at compile. The 2.0.8 fail-closed gates stay; only the focus *query* changed to the forms hosts compiled.
473
+ **macOS 26 focus compile (2.0.9).** Dual-host Sent prove on `@2.0.8` never reached paste: `atmSafeToPaste` used bare `focused UI element`, which System Events 26.x rejects at compile.
474
474
 
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). Size/position lists now use `atmW`/`atmH` (hosts confirmed) and other `atm*` names — never `th`/`tw`/`st`/`nd`/`rd`/`to`/`by`/`id` in destructuring. 2.0.8/2.0.9 fail-closed paste-focus and 2.0.9 AXFocusedUIElement / `whose focused is true` are unchanged.
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
- **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.
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 now **Tabs into the body** (runtime calibration against Mail `content of` the outgoing message, max Tab×6 as proven on Mini) and **types the body with Return between paragraphs**. Clipboard Cmd-V is only a fallback if the body needle is still missing. Fail-closed: missing needle 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
478
 
479
- **Manual prove (2.0.10 on the Mac host):** AppleScript for paste-focus **compiles** (no `-2741` on `th`; no `-2741`/`-2740` on `focused UI element`). Then `mail_send` a short 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`, and the intended text must be in the native body before send. Inspect each Sent `.emlx`: the text/plain part 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`, no **-2741** / **-2740** on body focus). That quote-prefix dual-host Sent prove is a separate bar from the 2.0.7 verify contract.
479
+ **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
+
481
+ **Manual prove (2.1.0 on the Mac host):** `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`, and the intended text must be in the native body before send, with newlines preserved. Inspect each Sent `.emlx`: the text/plain part 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.
480
482
 
481
483
  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.
482
484
 
@@ -170,6 +170,7 @@ export function needsHostTccAdvice(message) {
170
170
  const text = String(message || "");
171
171
  if (!text) return false;
172
172
  if (text.includes(MAIL_SEND_TIMEOUT_GUIDANCE)) return false;
173
+ if (text.includes(MAIL_GUI_SCRIPTING_GUIDANCE)) return false;
173
174
  if (text.includes(CONTACTS_APP_NOT_RUNNING_GUIDANCE)) return false;
174
175
  if (text.includes(MAIL_APP_NOT_RUNNING_GUIDANCE)) return false;
175
176
  if (text.includes(MESSAGES_APP_NOT_RUNNING_GUIDANCE)) return false;
@@ -339,10 +340,22 @@ export const MAIL_TCC_GUIDANCE =
339
340
  */
340
341
  export const MAIL_ACCESSIBILITY_GUIDANCE =
341
342
  "macOS denied Accessibility (System Events could not drive Mail's compose window). " +
342
- "mail_send / mail_draft paste the body so Mail does not cite-wrap it (FB11734014); Mail's mailto command does not return an outgoing message on current Mini/MacBook Mail (-2753). " +
343
+ "mail_send / mail_draft type or paste the body so Mail does not cite-wrap it (FB11734014); Mail's mailto command does not return an outgoing message on current Mini/MacBook Mail (-2753). " +
343
344
  "Allow node in System Settings → Privacy & Security → Accessibility, and keep Automation → Mail allowed. " +
344
345
  "This is not a Mail Automation deny (-1743 / -10004).";
345
346
 
347
+ /**
348
+ * System Events Apple Events / GUI scripting is missing, so the keystroke
349
+ * compose path cannot run. Distinct from a Mail send hang: nothing was typed
350
+ * or sent. Mini write-bridge should return unsupported so stdio falls back
351
+ * in-process rather than wedging ~60s.
352
+ */
353
+ export const MAIL_GUI_SCRIPTING_GUIDANCE =
354
+ "System Events GUI scripting is unavailable for this process, so the body cannot be typed into Mail. " +
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
+ "On a Mini LaunchAgent / write-bridge host, Allow the daemon's node → System Events in System Settings → Privacy & Security → Automation (in addition to node → Mail) and keep Accessibility allowed. " +
357
+ "This is not a Mail send timeout and not a Mail Automation deny (-1743 / -10004).";
358
+
346
359
  /**
347
360
  * AppleScript hung (ETIMEDOUT / -1712) while finding, opening, replying,
348
361
  * or sending. That is not -1743 / -10004, including before send.
@@ -385,7 +398,7 @@ export function tccGuidanceFor(source) {
385
398
  export function runAppleScript(script, options = {}) {
386
399
  const { timeout = DEFAULT_SCRIPT_TIMEOUT_MS, appName = null, language = null } = options;
387
400
  try {
388
- const output = safeOsascript(script, { timeout, language });
401
+ const output = safeOsascript(script, { timeout, language, killSignal: "SIGKILL" });
389
402
  return { ok: true, output: (output || "").trim(), error: null, kind: null };
390
403
  } catch (e) {
391
404
  const message = e && e.message ? e.message : String(e);
package/lib/mailWrite.js CHANGED
@@ -25,10 +25,12 @@ import {
25
25
  MAIL_TCC_GUIDANCE,
26
26
  MAIL_SEND_TIMEOUT_GUIDANCE,
27
27
  MAIL_ACCESSIBILITY_GUIDANCE,
28
+ MAIL_GUI_SCRIPTING_GUIDANCE,
28
29
  MAIL_APP_NOT_RUNNING_GUIDANCE,
29
30
  ATTRIBUTION_GUIDANCE,
30
31
  isHardTccDenial
31
32
  } from "./appleScript.js";
33
+ import { isIndexerMode } from "./processMode.js";
32
34
  import {
33
35
  planWrite,
34
36
  plannedWriteResult,
@@ -156,8 +158,21 @@ export function probeMailAutomation() {
156
158
  export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
157
159
  export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
158
160
  export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
159
- /** Compose body AX height vs To/Subject/Cc fields (those are ~22px). */
161
+ export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
162
+ export const SYSTEM_EVENTS_PROBE_MARKER = "ATM_SYSTEM_EVENTS_PROBE";
163
+ /** Compose body AX height vs To/Subject/Cc fields (those are ~22px). Kept for 2.0.8–2.0.10 compile tests; 2.1.0 compose does not walk AX. */
160
164
  export const MAIL_BODY_MIN_AX_HEIGHT = 50;
165
+ /** Mini-proven Tab count from default compose focus into the body. */
166
+ export const MAIL_BODY_TAB_MAX = 6;
167
+ /** Distinctive character typed then deleted to see whether the caret is in the body. */
168
+ export const MAIL_BODY_CALIBRATE_PROBE = "§";
169
+ /** Cheap System Events Automation probe; SIGKILL if it still hangs. */
170
+ export const MAIL_SE_PROBE_TIMEOUT_MS = 4000;
171
+ /** Compose osascript budget (make + keystroke + send). Far below the ~60s SE wedge. */
172
+ export const MAIL_COMPOSE_TIMEOUT_MS = 25000;
173
+ export const MAIL_SE_APPLEEVENT_TIMEOUT_SEC = 8;
174
+ export const MAIL_APPLEEVENT_TIMEOUT_SEC = 15;
175
+ export const MAIL_SE_PROBE_APPLEEVENT_TIMEOUT_SEC = 3;
161
176
 
162
177
  /**
163
178
  * AppleScript ordinal/keyword tokens that fail compile in `set {a, b}`
@@ -206,10 +221,10 @@ end atmFocusedElement`;
206
221
  }
207
222
 
208
223
  /**
209
- * Body text for native Mail compose (clipboard paste, not AppleScript content).
224
+ * Body text for native Mail compose (keystroke / paste, not AppleScript content).
210
225
  *
211
226
  * HTML is tag-stripped so a plain-looking HTML body (`<p>…</p>`) becomes
212
- * typed/pasted text. Never prefixes lines with `>` and never emits
227
+ * typed text. Never prefixes lines with `>` and never emits
213
228
  * `<blockquote>` — AppleScript `content` / `html content` cite-wrap
214
229
  * (FB11734014). Mail's `mailto` command is not used: on current Mini/MacBook
215
230
  * Mail it does not return an outgoing message (`newMessage` stays undefined,
@@ -231,213 +246,156 @@ export function composeBodyNeedle(body) {
231
246
  return needle.length > 120 ? needle.slice(0, 120) : needle;
232
247
  }
233
248
 
234
- const BODY_MIN_H = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
249
+ export function isSystemEventsProbeScript(script) {
250
+ return String(script || "").includes(SYSTEM_EVENTS_PROBE_MARKER);
251
+ }
252
+
253
+ export function buildSystemEventsProbeScript() {
254
+ const secs = asInteger(MAIL_SE_PROBE_APPLEEVENT_TIMEOUT_SEC, {
255
+ min: 1,
256
+ max: 15,
257
+ field: "seProbeTimeout"
258
+ });
259
+ return `-- ${SYSTEM_EVENTS_PROBE_MARKER}
260
+ with timeout of ${secs} seconds
261
+ tell application "System Events"
262
+ set atmName to name
263
+ if atmName is "" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
264
+ set atmUi to UI elements enabled
265
+ end tell
266
+ end timeout
267
+ if atmUi is false then error "${ACCESSIBILITY_DENIED_SENTINEL}"
268
+ return "OK"`;
269
+ }
270
+
271
+ export function probeSystemEventsGui() {
272
+ return runAppleScript(buildSystemEventsProbeScript(), {
273
+ timeout: MAIL_SE_PROBE_TIMEOUT_MS
274
+ });
275
+ }
235
276
 
236
277
  /**
237
- * AppleScript handlers: focus the compose body (not To/Subject) and paste.
238
- * System Events needs Accessibility; Mail make/send still needs Automation.
239
- *
240
- * macOS 26.x System Events has no `web area` class (compile -2741,
241
- * "Expected class name but found identifier") and no compound
242
- * `focused UI element` (compile -2741 / -2740). Hosts compile
243
- * `text area` / `scroll area` / `text field`, `UI element whose role is
244
- * "AXWebArea"`, `first UI element whose focused is true`, and
245
- * `value of attribute "AXFocusedUIElement"`. Never focus the first
246
- * AXTextArea blindly — on MacBook Mail that is a header field, and
247
- * Cmd-V becomes BODY_PASTE_MISDIRECTED.
248
- *
249
- * AppleScript treats ordinal suffixes as reserved tokens (`th` as in 4th,
250
- * also `st`/`nd`/`rd`). Dual-host Sent prove on 2.0.9 compiled past the
251
- * focused-UI fix, then failed in `atmFocusMailBody`:
252
- * `set {tw, th} to size of atmElem` → -2741 Expected expression but found
253
- * `th`. Size/position lists use `atmW`/`atmH` (hosts confirmed) and other
254
- * `atm*` names — never `th`/`tw`/`st`/`nd`/`rd`/`to`/`by`/`id` in
255
- * destructuring.
278
+ * AppleScript handlers: Tab into the compose body and type it (typed Returns
279
+ * preserve newlines). Does not walk System Events `windows of process Mail`
280
+ * (macOS 26 reports 0). Calibration reads Mail `content of` the outgoing
281
+ * message — never `set content` / `html content` (FB11734014 cite-wrap).
256
282
  *
257
- * Strategies, each fail-closed unless the caret left the header:
258
- * 1. Click a tall AXWebArea (HTML compose body)
259
- * 2. Focus a tall `text area` (plain compose body), never a short header
260
- * 3. Click Subject then Tab into the body (stable Mini/MacBook order)
261
- * 4. Click the lower two-thirds of the compose window
283
+ * Tab count is effect-calibrated up to MAIL_BODY_TAB_MAX (Mini-proven 6).
284
+ * Fail-closed if the body needle is missing after type (Cmd-V fallback) and
285
+ * header checks. System Events Apple events are wrapped in `with timeout`.
262
286
  */
263
287
  export function buildMailBodyPasteHandler() {
264
- return `${buildAtmFocusedElementHandler()}
265
-
266
- on atmSafeToPaste()
288
+ const tabMax = asInteger(MAIL_BODY_TAB_MAX, { min: 1, max: 12, field: "tabMax" });
289
+ const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
290
+ const probe = asString(MAIL_BODY_CALIBRATE_PROBE);
291
+ return `on atmSplitParagraphs(bodyText)
292
+ set atmSavedDelim to AppleScript's text item delimiters
293
+ set AppleScript's text item delimiters to return
294
+ set atmCrParts to text items of bodyText
295
+ set AppleScript's text item delimiters to linefeed
296
+ set atmNorm to atmCrParts as string
297
+ set atmLines to text items of atmNorm
298
+ set AppleScript's text item delimiters to atmSavedDelim
299
+ return atmLines
300
+ end atmSplitParagraphs
301
+
302
+ on atmSpacesForTabs(atmLine)
303
+ set atmSavedDelim to AppleScript's text item delimiters
304
+ set AppleScript's text item delimiters to tab
305
+ set atmTabParts to text items of atmLine
306
+ set AppleScript's text item delimiters to " "
307
+ set atmOut to atmTabParts as string
308
+ set AppleScript's text item delimiters to atmSavedDelim
309
+ return atmOut
310
+ end atmSpacesForTabs
311
+
312
+ on atmKeystrokeText(atmText)
313
+ set atmSafe to my atmSpacesForTabs(atmText)
314
+ set atmLen to length of atmSafe
315
+ if atmLen is 0 then return
316
+ set atmPos to 1
267
317
  tell application "System Events"
268
318
  tell process "Mail"
269
- try
270
- set fe to my atmFocusedElement()
271
- if fe is missing value then return false
272
- set r to (role of fe) as string
273
- if r is "AXWindow" then return false
274
- if r is "AXTextField" then return false
275
- if r is "AXComboBox" then return false
276
- if r is "AXButton" then return false
277
- if r is "AXMenuButton" then return false
278
- if r is "AXPopUpButton" then return false
279
- set atmH to 0
280
- try
281
- set {atmW, atmH} to size of fe
282
- end try
283
- if r is "AXTextArea" and atmH < ${BODY_MIN_H} then return false
284
- ignoring case
285
- set bits to ""
286
- try
287
- set bits to bits & (description of fe as string) & " "
288
- end try
289
- try
290
- set bits to bits & (name of fe as string) & " "
291
- end try
292
- if bits contains "To:" then return false
293
- if bits contains "Cc:" then return false
294
- if bits contains "Bcc:" then return false
295
- if bits contains "Subject" then return false
296
- end ignoring
297
- if r is "AXWebArea" then return true
298
- if r is "AXTextArea" then return true
299
- if atmH >= ${BODY_MIN_H} then return true
300
- end try
301
- return false
319
+ repeat while atmPos is less than or equal to atmLen
320
+ set atmEndPos to atmPos + 31
321
+ if atmEndPos > atmLen then set atmEndPos to atmLen
322
+ keystroke (text atmPos thru atmEndPos of atmSafe)
323
+ set atmPos to atmEndPos + 1
324
+ end repeat
302
325
  end tell
303
326
  end tell
304
- end atmSafeToPaste
327
+ end atmKeystrokeText
305
328
 
306
- on atmFocusMailBody(expectedSubject)
307
- tell application "System Events"
308
- tell process "Mail"
309
- set frontmost to true
310
- if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
311
- set w to window 1
312
- if expectedSubject is not "" then
313
- repeat with i from 1 to (count of windows)
314
- try
315
- if (name of window i as string) is expectedSubject then
316
- set w to window i
317
- exit repeat
318
- end if
319
- end try
320
- end repeat
321
- end if
322
- try
323
- perform action "AXRaise" of w
324
- end try
325
- try
326
- click (first UI element of w whose role is "AXWebArea")
327
- delay 0.08
328
- if my atmSafeToPaste() then return
329
- end try
330
- try
331
- click (first UI element of scroll area 1 of w whose role is "AXWebArea")
332
- delay 0.08
333
- if my atmSafeToPaste() then return
334
- end try
335
- try
336
- click (first UI element of scroll area 1 of splitter group 1 of w whose role is "AXWebArea")
337
- delay 0.08
338
- if my atmSafeToPaste() then return
339
- end try
340
- try
341
- click (first UI element of scroll area 1 of group 1 of splitter group 1 of w whose role is "AXWebArea")
342
- delay 0.08
343
- if my atmSafeToPaste() then return
344
- end try
345
- set atmTallText to missing value
346
- set atmSubjectElem to missing value
347
- set atmQueue to UI elements of w
348
- set atmWalked to 0
349
- repeat while (count of atmQueue) > 0 and atmWalked < 120
350
- set atmWalked to atmWalked + 1
351
- set atmElem to item 1 of atmQueue
352
- if (count of atmQueue) is 1 then
353
- set atmQueue to {}
354
- else
355
- set atmQueue to items 2 thru -1 of atmQueue
356
- end if
357
- try
358
- if (role of atmElem as string) is "AXWebArea" then
359
- set {atmW, atmH} to size of atmElem
360
- if atmH >= ${BODY_MIN_H} then
361
- click atmElem
362
- delay 0.08
363
- if my atmSafeToPaste() then return
364
- end if
365
- end if
366
- end try
367
- try
368
- if (role of atmElem as string) is "AXTextArea" then
369
- set {atmW, atmH} to size of atmElem
370
- if atmH >= ${BODY_MIN_H} and atmTallText is missing value then set atmTallText to atmElem
371
- end if
372
- end try
373
- try
374
- set r to (role of atmElem as string)
375
- if r is "AXTextField" or r is "AXTextArea" then
376
- set bits to ""
377
- try
378
- set bits to bits & (description of atmElem as string)
379
- end try
380
- try
381
- set bits to bits & (name of atmElem as string)
382
- end try
383
- ignoring case
384
- if bits contains "Subject" and atmSubjectElem is missing value then set atmSubjectElem to atmElem
385
- end ignoring
386
- end if
387
- end try
388
- try
389
- set atmKids to UI elements of atmElem
390
- repeat with i from 1 to (count of atmKids)
391
- set end of atmQueue to item i of atmKids
392
- end repeat
393
- end try
394
- end repeat
395
- try
396
- set ta to text area 1 of scroll area 1 of w
397
- set {atmW, atmH} to size of ta
398
- if atmH >= ${BODY_MIN_H} then
399
- set focused of ta to true
400
- delay 0.08
401
- if my atmSafeToPaste() then return
402
- end if
403
- end try
404
- if atmTallText is not missing value then
405
- try
406
- set focused of atmTallText to true
407
- end try
408
- try
409
- click atmTallText
410
- end try
411
- delay 0.08
412
- if my atmSafeToPaste() then return
413
- end if
414
- try
415
- click (first text field of w whose description contains "Subject")
416
- delay 0.08
417
- key code 48
418
- delay 0.08
419
- if my atmSafeToPaste() then return
420
- end try
421
- if atmSubjectElem is not missing value then
422
- try
423
- click atmSubjectElem
424
- delay 0.08
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
+ on atmTabIntoMailBody(msg)
340
+ tell application "Mail" to activate
341
+ delay 0.25
342
+ with timeout of ${seSecs} seconds
343
+ tell application "System Events"
344
+ tell process "Mail" to set frontmost to true
345
+ end tell
346
+ end timeout
347
+ delay 0.08
348
+ repeat with atmN from 0 to ${tabMax}
349
+ with timeout of ${seSecs} seconds
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
366
+ if atmN is ${tabMax} then error "${BODY_FOCUS_FAILED_SENTINEL}"
367
+ with timeout of ${seSecs} seconds
368
+ tell application "System Events"
369
+ tell process "Mail"
425
370
  key code 48
426
- delay 0.08
427
- if my atmSafeToPaste() then return
428
- end try
371
+ end tell
372
+ end tell
373
+ end timeout
374
+ delay 0.08
375
+ end repeat
376
+ error "${BODY_FOCUS_FAILED_SENTINEL}"
377
+ end atmTabIntoMailBody
378
+
379
+ on atmTypeMailBody(bodyText)
380
+ set atmLines to my atmSplitParagraphs(bodyText)
381
+ set atmLast to count of atmLines
382
+ if atmLast is 0 then return
383
+ with timeout of ${seSecs} seconds
384
+ repeat with atmI from 1 to atmLast
385
+ set atmLine to item atmI of atmLines as string
386
+ my atmKeystrokeText(atmLine)
387
+ if atmI < atmLast then
388
+ tell application "System Events"
389
+ tell process "Mail"
390
+ key code 36
391
+ end tell
392
+ end tell
429
393
  end if
430
- set {atmX, atmY} to position of w
431
- set {atmWinW, atmWinH} to size of w
432
- click at {(atmX + (atmWinW div 2)) as integer, (atmY + ((atmWinH * 2) div 3)) as integer}
433
- delay 0.08
434
- if my atmSafeToPaste() then return
435
- error "${BODY_FOCUS_FAILED_SENTINEL}"
436
- end tell
437
- end tell
438
- end atmFocusMailBody
394
+ end repeat
395
+ end timeout
396
+ end atmTypeMailBody
439
397
 
440
- on atmPasteMailBody(bodyText, expectedSubject)
398
+ on atmPasteMailBodyFallback(bodyText)
441
399
  set savedClip to ""
442
400
  set hadClip to false
443
401
  try
@@ -445,26 +403,43 @@ on atmPasteMailBody(bodyText, expectedSubject)
445
403
  set hadClip to true
446
404
  end try
447
405
  set the clipboard to bodyText
448
- tell application "Mail" to activate
449
- delay 0.4
450
406
  try
451
- atmFocusMailBody(expectedSubject)
452
- if atmSafeToPaste() is false then error "${BODY_FOCUS_FAILED_SENTINEL}"
453
- tell application "System Events"
454
- tell process "Mail"
455
- keystroke "v" using command down
407
+ with timeout of ${seSecs} seconds
408
+ tell application "System Events"
409
+ tell process "Mail"
410
+ keystroke "v" using command down
411
+ end tell
456
412
  end tell
457
- end tell
458
- delay 0.2
413
+ end timeout
414
+ delay 0.15
459
415
  on error errMsg number errNum
460
416
  if hadClip then set the clipboard to savedClip
461
417
  if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
462
418
  error errMsg number errNum
463
419
  end try
464
420
  if hadClip then set the clipboard to savedClip
465
- end atmPasteMailBody`;
421
+ end atmPasteMailBodyFallback
422
+
423
+ on atmFillMailBody(bodyText, needle, msg)
424
+ try
425
+ atmTabIntoMailBody(msg)
426
+ atmTypeMailBody(bodyText)
427
+ delay 0.12
428
+ if needle is not "" then
429
+ if my atmMailBodyContains(msg, needle) is false then
430
+ atmPasteMailBodyFallback(bodyText)
431
+ end if
432
+ end if
433
+ on error errMsg number errNum
434
+ if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
435
+ if errMsg contains "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
436
+ error errMsg number errNum
437
+ end try
438
+ end atmFillMailBody`;
466
439
  }
467
440
 
441
+ export const buildMailBodyFillHandler = buildMailBodyPasteHandler;
442
+
468
443
  function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
469
444
  const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
470
445
  const ccCount = asInteger(cc.length, { min: 0, max: 99, field: "ccCount" });
@@ -495,17 +470,6 @@ function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
495
470
  set atmGotBody to content of newMessage as string
496
471
  end try
497
472
  end if
498
- if atmGotBody does not contain ${asString(needle)} then
499
- try
500
- set atmFe to my atmFocusedElement()
501
- if atmFe is not missing value then
502
- tell application "System Events"
503
- set atmAxVal to value of atmFe as string
504
- if atmAxVal contains ${asString(needle)} then set atmGotBody to atmAxVal
505
- end tell
506
- end if
507
- end try
508
- end if
509
473
  if atmGotBody does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`
510
474
  : "";
511
475
  return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
@@ -526,9 +490,10 @@ ${bodyCheck}`;
526
490
  * Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
527
491
  * cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
528
492
  * Recipients and subject are AppleScript properties (never paste). The body
529
- * is pasted into the compose window (native editor) via System Events only
530
- * after caret focus leaves the header. If paste lands in To/Subject, or the
531
- * body never receives the intended text, abort + delete (nothing sent).
493
+ * is typed into the native compose editor via System Events (Tab into body,
494
+ * typed Returns for newlines, Cmd-V only if the body needle is still
495
+ * missing). Do not walk `windows of process Mail`. If the body never
496
+ * receives the intended text, or headers change, abort + delete (nothing sent).
532
497
  */
533
498
  export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
534
499
  const recipients = [
@@ -537,9 +502,11 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
537
502
  recipientLines(bcc, "bcc recipient")
538
503
  ].filter((block) => block.length > 0).join("\n");
539
504
 
505
+ const mailSecs = asInteger(MAIL_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 60, field: "mailTimeout" });
540
506
  const plainBody = composeNativeBody(body, { html });
507
+ const needle = composeBodyNeedle(plainBody);
541
508
  const pasteAndVerify = plainBody
542
- ? ` atmPasteMailBody(${asString(plainBody)}, ${asString(subject)})
509
+ ? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, newMessage)
543
510
  tell application "Mail"
544
511
  ${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
545
512
  end tell`
@@ -548,11 +515,13 @@ ${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
548
515
  return `${buildMailBodyPasteHandler()}
549
516
 
550
517
  tell application "Mail"
551
- set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
552
- tell newMessage
518
+ with timeout of ${mailSecs} seconds
519
+ set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
520
+ tell newMessage
553
521
  ${recipients}
554
- end tell
555
- activate
522
+ end tell
523
+ activate
524
+ end timeout
556
525
  end tell
557
526
  try
558
527
  ${pasteAndVerify}
@@ -564,10 +533,12 @@ on error errMsg number errNum
564
533
  end try
565
534
  set atmOutgoingId to ""
566
535
  tell application "Mail"
567
- try
568
- set atmOutgoingId to message id of newMessage as string
569
- end try
570
- ${send ? "send newMessage" : "save newMessage"}
536
+ with timeout of ${mailSecs} seconds
537
+ try
538
+ set atmOutgoingId to message id of newMessage as string
539
+ end try
540
+ ${send ? "send newMessage" : "save newMessage"}
541
+ end timeout
571
542
  end tell
572
543
  if atmOutgoingId is not "" then return atmOutgoingId
573
544
  return "OK"`;
@@ -927,6 +898,14 @@ export function verifyQueuedMessage(match = {}, { attempts = SENT_VERIFY_ATTEMPT
927
898
  return last;
928
899
  }
929
900
 
901
+ export function isMailGuiScriptingUnavailable(result) {
902
+ if (!result) return false;
903
+ const text = String(result.error || "").toLowerCase();
904
+ if (text.includes("gui_scripting_unavailable")) return true;
905
+ if (result.kind === "timeout" && text.includes("system events")) return true;
906
+ return false;
907
+ }
908
+
930
909
  export function isMailAccessibilityDenial(result) {
931
910
  if (!result) return false;
932
911
  const text = String(result.error || "").toLowerCase();
@@ -946,6 +925,9 @@ export function isMailBodyPasteMisdirected(result) {
946
925
 
947
926
  function failure(action, summary, result, secrets) {
948
927
  const err = String((result && result.error) || "").toLowerCase();
928
+ if (err.includes("gui_scripting_unavailable")) {
929
+ return `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
930
+ }
949
931
  if (err.includes("body_focus_failed")) {
950
932
  return `${action} failed — attempted to ${summary}. The compose caret could not be moved into the message body; nothing was sent.`;
951
933
  }
@@ -1051,10 +1033,24 @@ function finalizeMailWrite({
1051
1033
  return { ok: false, delivered: false, mailbox: null, message: failure(action, summary, result, secrets) };
1052
1034
  }
1053
1035
 
1036
+ function guiScriptingUnavailableResult(action, summary, result, { indexerMode } = {}) {
1037
+ const unsupported = Boolean(indexerMode);
1038
+ const message = isMailAccessibilityDenial(result)
1039
+ ? `${action} failed — attempted to ${summary}. ${MAIL_ACCESSIBILITY_GUIDANCE}`
1040
+ : `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
1041
+ return {
1042
+ ok: false,
1043
+ delivered: false,
1044
+ mailbox: null,
1045
+ ...(unsupported ? { unsupported: true } : {}),
1046
+ message
1047
+ };
1048
+ }
1049
+
1054
1050
  /**
1055
1051
  * Compose and send (or save as draft) a new email.
1056
1052
  */
1057
- export function mailCompose(args = {}, { draft = false } = {}) {
1053
+ export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerMode() } = {}) {
1058
1054
  const action = draft ? "mail_draft" : "mail_send";
1059
1055
 
1060
1056
  const to = validateEmailList(args.to, "to");
@@ -1093,6 +1089,14 @@ export function mailCompose(args = {}, { draft = false } = {}) {
1093
1089
  });
1094
1090
  if (!plan.proceed) return plannedWriteResult(plan);
1095
1091
 
1092
+ const plainBody = composeNativeBody(body.text, { html: bodyFormat === "html" });
1093
+ if (plainBody) {
1094
+ const se = probeSystemEventsGui();
1095
+ if (!se.ok) {
1096
+ return guiScriptingUnavailableResult(action, summary, se, { indexerMode });
1097
+ }
1098
+ }
1099
+
1096
1100
  const script = buildComposeScript({
1097
1101
  to: to.addresses,
1098
1102
  cc: cc.addresses,
@@ -1103,7 +1107,14 @@ export function mailCompose(args = {}, { draft = false } = {}) {
1103
1107
  html: bodyFormat === "html"
1104
1108
  });
1105
1109
 
1106
- const result = runAppleScript(script, { timeout: 60000, appName: "Mail" });
1110
+ const result = runAppleScript(script, { timeout: MAIL_COMPOSE_TIMEOUT_MS, appName: "Mail" });
1111
+ if (
1112
+ !result.ok &&
1113
+ indexerMode &&
1114
+ (isMailGuiScriptingUnavailable(result) || isMailAccessibilityDenial(result))
1115
+ ) {
1116
+ return guiScriptingUnavailableResult(action, summary, result, { indexerMode });
1117
+ }
1107
1118
  return finalizeMailWrite({
1108
1119
  action,
1109
1120
  summary,
package/lib/shell.js CHANGED
@@ -131,9 +131,12 @@ export function safeOsascript(script, options = {}) {
131
131
  args.push("-e", script);
132
132
 
133
133
  // Use -e with the script as an argument (never a shell string).
134
+ // Hung Apple Events ignore SIGTERM (Mini write-bridge ~60s wedge).
135
+ // Timeout must SIGKILL osascript so compose fails fast instead of hanging.
134
136
  const result = spawnSync('osascript', args, {
135
137
  encoding: 'utf-8',
136
138
  timeout,
139
+ killSignal: 'SIGKILL',
137
140
  shell: false
138
141
  });
139
142
 
@@ -102,7 +102,7 @@ export function terminalAutomationAdvice(execPath = process.execPath) {
102
102
  export function miniLaunchAgentAdvice() {
103
103
  return (
104
104
  "This Mac has a Mini write-bridge / LaunchAgent. Grant the daemon's node Full Disk Access (reads) and " +
105
- "Allow that node in System Settings → Privacy & Security → Automation for Mail.app, Messages.app, Contacts.app, and Calendar.app. " +
105
+ "Allow that node in System Settings → Privacy & Security → Automation for Mail.app, Messages.app, Contacts.app, Calendar.app, and System Events (mail_send / mail_draft type the body via keystrokes). " +
106
106
  "Do not add node via + in the Contacts or Calendars privacy lists."
107
107
  );
108
108
  }
@@ -127,7 +127,7 @@ export function tccFallbackAdvice({
127
127
  if (mini && bridgeAvailable) {
128
128
  return (
129
129
  "The indexer daemon was reachable but the write was still denied; grant the daemon's node binary Full Disk Access (reads) and " +
130
- "Allow node in System Settings > Privacy & Security > Automation for Mail.app, Messages.app, Contacts.app, and Calendar.app. " +
130
+ "Allow node in System Settings > Privacy & Security > Automation for Mail.app, Messages.app, Contacts.app, Calendar.app, and System Events. " +
131
131
  "A hang or timeout on Mail compose is TCC / Automation denied, not Mail.app missing. " +
132
132
  "Do not add node via + in the Contacts or Calendars privacy lists — those panes often have no Add button."
133
133
  );
package/lib/writeTools.js CHANGED
@@ -57,7 +57,7 @@ export const WRITE_TOOL_DEFINITIONS = [
57
57
  bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
58
58
  subject: { type: "string", description: "Subject line (required)" },
59
59
  body: { type: "string", description: "Message body (required)" },
60
- body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped and pasted into Mail's compose window; AppleScript content/html content is not set because it quote-wraps the Sent body" },
60
+ body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped; the body is typed into Mail's compose window (System Events keystrokes). AppleScript content/html content is not set because it quote-wraps the Sent body" },
61
61
  ...CONFIRM_PROPS
62
62
  },
63
63
  required: ["to", "subject", "body"]
@@ -74,7 +74,7 @@ export const WRITE_TOOL_DEFINITIONS = [
74
74
  bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
75
75
  subject: { type: "string", description: "Subject line" },
76
76
  body: { type: "string", description: "Message body" },
77
- body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped and pasted into Mail's compose window; AppleScript content/html content is not set because it quote-wraps the Sent body" },
77
+ body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped; the body is typed into Mail's compose window (System Events keystrokes). AppleScript content/html content is not set because it quote-wraps the Sent body" },
78
78
  ...CONFIRM_PROPS
79
79
  },
80
80
  required: ["to"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-tools-mcp",
3
- "version": "2.0.10",
3
+ "version": "2.1.0",
4
4
  "description": "MCP server for semantic search and write actions across Apple Mail, Messages, Calendar, and Contacts",
5
5
  "type": "module",
6
6
  "main": "index.js",