apple-tools-mcp 2.0.9 → 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,17 +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
- **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.
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
- **Manual prove (2.0.9 on the Mac host):** AppleScript for safe-to-paste / focus check **compiles** (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.
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
+
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.
478
482
 
479
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.
480
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,36 @@ 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;
176
+
177
+ /**
178
+ * AppleScript ordinal/keyword tokens that fail compile in `set {a, b}`
179
+ * lists on macOS 26.x (MacBook 26.1 + Mini 26.6.2: `th` → -2741).
180
+ * Paste-focus handlers must not destructure into these names.
181
+ */
182
+ export const ATM_APPLESCRIPT_RESERVED_SHORTS = Object.freeze([
183
+ "th",
184
+ "st",
185
+ "nd",
186
+ "rd",
187
+ "to",
188
+ "by",
189
+ "id"
190
+ ]);
161
191
 
162
192
  /**
163
193
  * macOS 26.x System Events rejects the compound `focused UI element`
@@ -191,10 +221,10 @@ end atmFocusedElement`;
191
221
  }
192
222
 
193
223
  /**
194
- * Body text for native Mail compose (clipboard paste, not AppleScript content).
224
+ * Body text for native Mail compose (keystroke / paste, not AppleScript content).
195
225
  *
196
226
  * HTML is tag-stripped so a plain-looking HTML body (`<p>…</p>`) becomes
197
- * typed/pasted text. Never prefixes lines with `>` and never emits
227
+ * typed text. Never prefixes lines with `>` and never emits
198
228
  * `<blockquote>` — AppleScript `content` / `html content` cite-wrap
199
229
  * (FB11734014). Mail's `mailto` command is not used: on current Mini/MacBook
200
230
  * Mail it does not return an outgoing message (`newMessage` stays undefined,
@@ -216,205 +246,156 @@ export function composeBodyNeedle(body) {
216
246
  return needle.length > 120 ? needle.slice(0, 120) : needle;
217
247
  }
218
248
 
219
- 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
+ }
220
276
 
221
277
  /**
222
- * AppleScript handlers: focus the compose body (not To/Subject) and paste.
223
- * System Events needs Accessibility; Mail make/send still needs Automation.
224
- *
225
- * macOS 26.x System Events has no `web area` class (compile -2741,
226
- * "Expected class name but found identifier") and no compound
227
- * `focused UI element` (compile -2741 / -2740). Hosts compile
228
- * `text area` / `scroll area` / `text field`, `UI element whose role is
229
- * "AXWebArea"`, `first UI element whose focused is true`, and
230
- * `value of attribute "AXFocusedUIElement"`. Never focus the first
231
- * AXTextArea blindly — on MacBook Mail that is a header field, and
232
- * Cmd-V becomes BODY_PASTE_MISDIRECTED.
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).
233
282
  *
234
- * Strategies, each fail-closed unless the caret left the header:
235
- * 1. Click a tall AXWebArea (HTML compose body)
236
- * 2. Focus a tall `text area` (plain compose body), never a short header
237
- * 3. Click Subject then Tab into the body (stable Mini/MacBook order)
238
- * 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`.
239
286
  */
240
287
  export function buildMailBodyPasteHandler() {
241
- return `${buildAtmFocusedElementHandler()}
242
-
243
- 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
244
317
  tell application "System Events"
245
318
  tell process "Mail"
246
- try
247
- set fe to my atmFocusedElement()
248
- if fe is missing value then return false
249
- set r to (role of fe) as string
250
- if r is "AXWindow" then return false
251
- if r is "AXTextField" then return false
252
- if r is "AXComboBox" then return false
253
- if r is "AXButton" then return false
254
- if r is "AXMenuButton" then return false
255
- if r is "AXPopUpButton" then return false
256
- set eh to 0
257
- try
258
- set {ew, eh} to size of fe
259
- end try
260
- if r is "AXTextArea" and eh < ${BODY_MIN_H} then return false
261
- ignoring case
262
- set bits to ""
263
- try
264
- set bits to bits & (description of fe as string) & " "
265
- end try
266
- try
267
- set bits to bits & (name of fe as string) & " "
268
- end try
269
- if bits contains "To:" then return false
270
- if bits contains "Cc:" then return false
271
- if bits contains "Bcc:" then return false
272
- if bits contains "Subject" then return false
273
- end ignoring
274
- if r is "AXWebArea" then return true
275
- if r is "AXTextArea" then return true
276
- if eh >= ${BODY_MIN_H} then return true
277
- end try
278
- 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
279
325
  end tell
280
326
  end tell
281
- end atmSafeToPaste
327
+ end atmKeystrokeText
282
328
 
283
- on atmFocusMailBody(expectedSubject)
284
- tell application "System Events"
285
- tell process "Mail"
286
- set frontmost to true
287
- if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
288
- set w to window 1
289
- if expectedSubject is not "" then
290
- repeat with i from 1 to (count of windows)
291
- try
292
- if (name of window i as string) is expectedSubject then
293
- set w to window i
294
- exit repeat
295
- end if
296
- end try
297
- end repeat
298
- end if
299
- try
300
- perform action "AXRaise" of w
301
- end try
302
- try
303
- click (first UI element of w whose role is "AXWebArea")
304
- delay 0.08
305
- if my atmSafeToPaste() then return
306
- end try
307
- try
308
- click (first UI element of scroll area 1 of w whose role is "AXWebArea")
309
- delay 0.08
310
- if my atmSafeToPaste() then return
311
- end try
312
- try
313
- click (first UI element of scroll area 1 of splitter group 1 of w whose role is "AXWebArea")
314
- delay 0.08
315
- if my atmSafeToPaste() then return
316
- end try
317
- try
318
- click (first UI element of scroll area 1 of group 1 of splitter group 1 of w whose role is "AXWebArea")
319
- delay 0.08
320
- if my atmSafeToPaste() then return
321
- end try
322
- set atmTallText to missing value
323
- set atmSubjectElem to missing value
324
- set atmQueue to UI elements of w
325
- set atmWalked to 0
326
- repeat while (count of atmQueue) > 0 and atmWalked < 120
327
- set atmWalked to atmWalked + 1
328
- set atmElem to item 1 of atmQueue
329
- if (count of atmQueue) is 1 then
330
- set atmQueue to {}
331
- else
332
- set atmQueue to items 2 thru -1 of atmQueue
333
- end if
334
- try
335
- if (role of atmElem as string) is "AXWebArea" then
336
- set {ew, eh} to size of atmElem
337
- if eh >= ${BODY_MIN_H} then
338
- click atmElem
339
- delay 0.08
340
- if my atmSafeToPaste() then return
341
- end if
342
- end if
343
- end try
344
- try
345
- if (role of atmElem as string) is "AXTextArea" then
346
- set {tw, th} to size of atmElem
347
- if th >= ${BODY_MIN_H} and atmTallText is missing value then set atmTallText to atmElem
348
- end if
349
- end try
350
- try
351
- set r to (role of atmElem as string)
352
- if r is "AXTextField" or r is "AXTextArea" then
353
- set bits to ""
354
- try
355
- set bits to bits & (description of atmElem as string)
356
- end try
357
- try
358
- set bits to bits & (name of atmElem as string)
359
- end try
360
- ignoring case
361
- if bits contains "Subject" and atmSubjectElem is missing value then set atmSubjectElem to atmElem
362
- end ignoring
363
- end if
364
- end try
365
- try
366
- set atmKids to UI elements of atmElem
367
- repeat with i from 1 to (count of atmKids)
368
- set end of atmQueue to item i of atmKids
369
- end repeat
370
- end try
371
- end repeat
372
- try
373
- set ta to text area 1 of scroll area 1 of w
374
- set {tw, th} to size of ta
375
- if th >= ${BODY_MIN_H} then
376
- set focused of ta to true
377
- delay 0.08
378
- if my atmSafeToPaste() then return
379
- end if
380
- end try
381
- if atmTallText is not missing value then
382
- try
383
- set focused of atmTallText to true
384
- end try
385
- try
386
- click atmTallText
387
- end try
388
- delay 0.08
389
- if my atmSafeToPaste() then return
390
- end if
391
- try
392
- click (first text field of w whose description contains "Subject")
393
- delay 0.08
394
- key code 48
395
- delay 0.08
396
- if my atmSafeToPaste() then return
397
- end try
398
- if atmSubjectElem is not missing value then
399
- try
400
- click atmSubjectElem
401
- 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"
402
370
  key code 48
403
- delay 0.08
404
- if my atmSafeToPaste() then return
405
- 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
406
393
  end if
407
- set {wx, wy} to position of w
408
- set {ww, wh} to size of w
409
- click at {(wx + (ww div 2)) as integer, (wy + ((wh * 2) div 3)) as integer}
410
- delay 0.08
411
- if my atmSafeToPaste() then return
412
- error "${BODY_FOCUS_FAILED_SENTINEL}"
413
- end tell
414
- end tell
415
- end atmFocusMailBody
394
+ end repeat
395
+ end timeout
396
+ end atmTypeMailBody
416
397
 
417
- on atmPasteMailBody(bodyText, expectedSubject)
398
+ on atmPasteMailBodyFallback(bodyText)
418
399
  set savedClip to ""
419
400
  set hadClip to false
420
401
  try
@@ -422,26 +403,43 @@ on atmPasteMailBody(bodyText, expectedSubject)
422
403
  set hadClip to true
423
404
  end try
424
405
  set the clipboard to bodyText
425
- tell application "Mail" to activate
426
- delay 0.4
427
406
  try
428
- atmFocusMailBody(expectedSubject)
429
- if atmSafeToPaste() is false then error "${BODY_FOCUS_FAILED_SENTINEL}"
430
- tell application "System Events"
431
- tell process "Mail"
432
- 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
433
412
  end tell
434
- end tell
435
- delay 0.2
413
+ end timeout
414
+ delay 0.15
436
415
  on error errMsg number errNum
437
416
  if hadClip then set the clipboard to savedClip
438
417
  if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
439
418
  error errMsg number errNum
440
419
  end try
441
420
  if hadClip then set the clipboard to savedClip
442
- 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`;
443
439
  }
444
440
 
441
+ export const buildMailBodyFillHandler = buildMailBodyPasteHandler;
442
+
445
443
  function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
446
444
  const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
447
445
  const ccCount = asInteger(cc.length, { min: 0, max: 99, field: "ccCount" });
@@ -472,17 +470,6 @@ function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
472
470
  set atmGotBody to content of newMessage as string
473
471
  end try
474
472
  end if
475
- if atmGotBody does not contain ${asString(needle)} then
476
- try
477
- set atmFe to my atmFocusedElement()
478
- if atmFe is not missing value then
479
- tell application "System Events"
480
- set atmAxVal to value of atmFe as string
481
- if atmAxVal contains ${asString(needle)} then set atmGotBody to atmAxVal
482
- end tell
483
- end if
484
- end try
485
- end if
486
473
  if atmGotBody does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`
487
474
  : "";
488
475
  return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
@@ -503,9 +490,10 @@ ${bodyCheck}`;
503
490
  * Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
504
491
  * cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
505
492
  * Recipients and subject are AppleScript properties (never paste). The body
506
- * is pasted into the compose window (native editor) via System Events only
507
- * after caret focus leaves the header. If paste lands in To/Subject, or the
508
- * 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).
509
497
  */
510
498
  export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
511
499
  const recipients = [
@@ -514,9 +502,11 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
514
502
  recipientLines(bcc, "bcc recipient")
515
503
  ].filter((block) => block.length > 0).join("\n");
516
504
 
505
+ const mailSecs = asInteger(MAIL_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 60, field: "mailTimeout" });
517
506
  const plainBody = composeNativeBody(body, { html });
507
+ const needle = composeBodyNeedle(plainBody);
518
508
  const pasteAndVerify = plainBody
519
- ? ` atmPasteMailBody(${asString(plainBody)}, ${asString(subject)})
509
+ ? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, newMessage)
520
510
  tell application "Mail"
521
511
  ${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
522
512
  end tell`
@@ -525,11 +515,13 @@ ${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
525
515
  return `${buildMailBodyPasteHandler()}
526
516
 
527
517
  tell application "Mail"
528
- set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
529
- 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
530
521
  ${recipients}
531
- end tell
532
- activate
522
+ end tell
523
+ activate
524
+ end timeout
533
525
  end tell
534
526
  try
535
527
  ${pasteAndVerify}
@@ -541,10 +533,12 @@ on error errMsg number errNum
541
533
  end try
542
534
  set atmOutgoingId to ""
543
535
  tell application "Mail"
544
- try
545
- set atmOutgoingId to message id of newMessage as string
546
- end try
547
- ${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
548
542
  end tell
549
543
  if atmOutgoingId is not "" then return atmOutgoingId
550
544
  return "OK"`;
@@ -904,6 +898,14 @@ export function verifyQueuedMessage(match = {}, { attempts = SENT_VERIFY_ATTEMPT
904
898
  return last;
905
899
  }
906
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
+
907
909
  export function isMailAccessibilityDenial(result) {
908
910
  if (!result) return false;
909
911
  const text = String(result.error || "").toLowerCase();
@@ -923,6 +925,9 @@ export function isMailBodyPasteMisdirected(result) {
923
925
 
924
926
  function failure(action, summary, result, secrets) {
925
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
+ }
926
931
  if (err.includes("body_focus_failed")) {
927
932
  return `${action} failed — attempted to ${summary}. The compose caret could not be moved into the message body; nothing was sent.`;
928
933
  }
@@ -1028,10 +1033,24 @@ function finalizeMailWrite({
1028
1033
  return { ok: false, delivered: false, mailbox: null, message: failure(action, summary, result, secrets) };
1029
1034
  }
1030
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
+
1031
1050
  /**
1032
1051
  * Compose and send (or save as draft) a new email.
1033
1052
  */
1034
- export function mailCompose(args = {}, { draft = false } = {}) {
1053
+ export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerMode() } = {}) {
1035
1054
  const action = draft ? "mail_draft" : "mail_send";
1036
1055
 
1037
1056
  const to = validateEmailList(args.to, "to");
@@ -1070,6 +1089,14 @@ export function mailCompose(args = {}, { draft = false } = {}) {
1070
1089
  });
1071
1090
  if (!plan.proceed) return plannedWriteResult(plan);
1072
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
+
1073
1100
  const script = buildComposeScript({
1074
1101
  to: to.addresses,
1075
1102
  cc: cc.addresses,
@@ -1080,7 +1107,14 @@ export function mailCompose(args = {}, { draft = false } = {}) {
1080
1107
  html: bodyFormat === "html"
1081
1108
  });
1082
1109
 
1083
- 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
+ }
1084
1118
  return finalizeMailWrite({
1085
1119
  action,
1086
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.9",
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",