apple-tools-mcp 2.1.2 → 2.1.4
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 +9 -3
- package/lib/appleScript.js +2 -2
- package/lib/mailWrite.js +275 -97
- package/lib/writeRouting.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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, Calendar.app, **and System Events** (mail_send / mail_draft
|
|
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 fill the body via AX hit-test). 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,7 +464,7 @@ 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
|
|
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 fill the body into the compose window (subject title-marker + coordinate AX hit-test, then AXValue). 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
469
|
Keystrokes use System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) **and** Automation → Mail **and** Automation → System Events. Run `apple-tools-mcp permissions` after first install or upgrade so **node → System Events** pops **Allow** in the same sitting as Mail / Messages / Contacts / Calendar. An Accessibility / System Events deny is not a Mail Automation deny (`-1743` / `-10004`).
|
|
470
470
|
|
|
@@ -478,9 +478,15 @@ Keystrokes use System Events, so **node needs Accessibility** (Privacy & Securit
|
|
|
478
478
|
|
|
479
479
|
**AX body-focus oracle (2.1.1).** Dual-host Mail 16 / macOS 26 showed that Mail AppleScript `content of` the outgoing message is **not** a live oracle for UI-typed/pasted text: `set content` reads back, but after Tab + keystroke/paste into the UI body `content of` stays empty (`typedSeen=false`, bodyLen 0) even when typing still produces a plain Sent `.emlx`. `@2.1.0` calibrated Tab against that property and fail-closed with `BODY_FOCUS_FAILED`. 2.1.1 Tabs until the focused AX role is a body field (`AXWebArea` / non-header `AXTextArea` via `AXFocusedUIElement` / `first UI element whose focused is true` — never `focused UI element`, never `windows of process Mail`). Cmd-V runs only when that AX **value** is readable and still missing the needle. An empty AX value is not a miss (Mail 16 WebArea); header checks still fail-closed. Ship prove that the body landed remains the Sent `.emlx` after send (2.0.7 To+subject verify). Do not “fix Tab count.”
|
|
480
480
|
|
|
481
|
+
**Compose body hit-test (2.1.3).** Mini host: `AXFocusedUIElement` role coerce fails (**-1700**); `windows of process Mail` is empty; a deep `UI elements` tree walk (the `app_ax_find` class of walk) **truncates after ~23 header/toolbar nodes** and never reaches compose `AXWebArea`. Subject-anchored clipboard read-back is refuted on Mini. The proven path: identify the compose window by the **subject title** among **top-level** System Events UI elements, then **coordinate AX hit-test** (`AXUIElementCopyElementAtPosition`) into the body region. The hit must be `AXWebArea` **"message body"**; a short `AXTextField` is fail-closed (`BODY_FOCUS_FAILED`). Focus + `AXValue` produced a **plain** Sent `.emlx` (cite=0) — no AppleScript `content` setter, no physical mouse click. Typed Returns / Cmd-V run only if a readable AX value is still missing the needle. System Events permissions probe is unchanged from 2.1.2.
|
|
482
|
+
|
|
483
|
+
**AXTitle compile (2.1.4).** Mini host: 2.1.3 `mail_send` never reached hit-test because `osacompile` failed **-2741** on bare `value of attribute "AXTitle" of atmEl` in `atmTopElemBits` / `atmTopElemExactTitle` (those handlers sit outside the System Events tell that walks top-level UI elements). Mini solution probe (`results-214/SOLUTION-PROBE-REPORT.md`): wrapping those AXTitle reads inside `tell application "System Events"` compiles (`osacompile` exit 0). Hit-test + fail-closed `AXTextField` + plain Sent behavior is unchanged from 2.1.3. Live Sent prove is after npm.
|
|
484
|
+
|
|
481
485
|
**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`.
|
|
482
486
|
|
|
483
|
-
**
|
|
487
|
+
**Compile prove (2.1.4 on the Mini host):** Unpatched `buildComposeScript` / `buildMailBodyPasteHandler` `osacompile` **-2741** on those bare AXTitle reads; patched AXTitle-inside-System-Events tell `osacompile` exit 0. Report: `results-214/SOLUTION-PROBE-REPORT.md`. Global install untouched. Live Sent is the 2.1.3 ship prove after npm.
|
|
488
|
+
|
|
489
|
+
**Manual prove (2.1.3 on the Mini host):** After `apple-tools-mcp permissions` reports System Events granted, `mail_send` a short **multiline** plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. The body must land via title-marker + hit-test (not Tab / focused-role): no `BODY_FOCUS_FAILED` / `BODY_PASTE_MISDIRECTED`. Inspect each Sent `.emlx`: the text/plain part must contain the intended text (newlines preserved) and must not prefix every body line with `>`, and any HTML alternative must not wrap the whole body in `<blockquote type="cite">`. Compose must succeed (no `-2753` / undefined `newMessage`). Prove the production path (write-bridge with node → System Events allowed, **or** the documented in-process fallback). That quote-prefix Mini Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
484
490
|
|
|
485
491
|
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.
|
|
486
492
|
|
package/lib/appleScript.js
CHANGED
|
@@ -340,7 +340,7 @@ export const MAIL_TCC_GUIDANCE =
|
|
|
340
340
|
*/
|
|
341
341
|
export const MAIL_ACCESSIBILITY_GUIDANCE =
|
|
342
342
|
"macOS denied Accessibility (System Events could not drive Mail's compose window). " +
|
|
343
|
-
"mail_send / mail_draft
|
|
343
|
+
"mail_send / mail_draft fill the body via AX hit-test so Mail does not cite-wrap it (FB11734014); Mail's mailto command does not return an outgoing message on current Mini/MacBook Mail (-2753). " +
|
|
344
344
|
"Allow node in System Settings → Privacy & Security → Accessibility, and keep Automation → Mail allowed. " +
|
|
345
345
|
"This is not a Mail Automation deny (-1743 / -10004).";
|
|
346
346
|
|
|
@@ -355,7 +355,7 @@ export const SYSTEM_EVENTS_STICKY_DENY_GUIDANCE =
|
|
|
355
355
|
/** System Events GUI scripting missing — not a Mail send hang. */
|
|
356
356
|
export const MAIL_GUI_SCRIPTING_GUIDANCE =
|
|
357
357
|
"System Events GUI scripting is unavailable for this process, so the body cannot be typed into Mail. " +
|
|
358
|
-
"mail_send / mail_draft
|
|
358
|
+
"mail_send / mail_draft locate the compose body via System Events (title-marker + AX hit-test) so Mail does not cite-wrap the Sent body (FB11734014). " +
|
|
359
359
|
"Allow node → System Events in System Settings → Privacy & Security → Automation (run apple-tools-mcp permissions after install/upgrade) in addition to node → Mail, and keep Accessibility allowed. " +
|
|
360
360
|
SYSTEM_EVENTS_STICKY_DENY_GUIDANCE + " " +
|
|
361
361
|
"This is not a Mail send timeout and not a Mail Automation deny (-1743 / -10004).";
|
package/lib/mailWrite.js
CHANGED
|
@@ -162,10 +162,16 @@ export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
|
|
|
162
162
|
export const SYSTEM_EVENTS_PROBE_MARKER = "ATM_SYSTEM_EVENTS_PROBE";
|
|
163
163
|
/** Permissions-only marker: process / GUI scripting, not application `name`. */
|
|
164
164
|
export const SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER = "ATM_SYSTEM_EVENTS_AUTOMATION_PROBE";
|
|
165
|
-
/** Compose body AX height vs To/Subject/Cc fields (those are ~22px). Used to reject short header AXTextAreas
|
|
165
|
+
/** Compose body AX height vs To/Subject/Cc fields (those are ~22px). Used to reject short header AXTextAreas; does not walk windows. */
|
|
166
166
|
export const MAIL_BODY_MIN_AX_HEIGHT = 50;
|
|
167
|
-
/** Mini
|
|
167
|
+
/** Historical Mini Tab count. 2.1.3 finds the body via title-marker + AX hit-test, not Tab. */
|
|
168
168
|
export const MAIL_BODY_TAB_MAX = 6;
|
|
169
|
+
/** Mail window titles may truncate; match this many leading subject characters. */
|
|
170
|
+
export const MAIL_COMPOSE_TITLE_MARKER_MAX = 60;
|
|
171
|
+
/** Top-level `UI elements` of process Mail only — never a deep tree walk. */
|
|
172
|
+
export const MAIL_COMPOSE_TOP_UI_MAX = 48;
|
|
173
|
+
/** Marker inside the JXA hit-test so tests can pin the proven Mini path. */
|
|
174
|
+
export const ATM_AX_HIT_TEST_MARKER = "ATM_AX_HIT_TEST";
|
|
169
175
|
/** Cheap pre-compose System Events check; SIGKILL if it still hangs. Not the permissions grant. */
|
|
170
176
|
export const MAIL_SE_PROBE_TIMEOUT_MS = 4000;
|
|
171
177
|
/** First-run / upgrade permissions probe: long enough for the Allow click. */
|
|
@@ -248,6 +254,101 @@ export function composeBodyNeedle(body) {
|
|
|
248
254
|
return needle.length > 120 ? needle.slice(0, 120) : needle;
|
|
249
255
|
}
|
|
250
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Subject fragment used to identify the compose window among top-level
|
|
259
|
+
* System Events UI elements. Mail's window title is the subject after
|
|
260
|
+
* `make new outgoing message`; empty drafts show "New Message".
|
|
261
|
+
*/
|
|
262
|
+
export function composeTitleMarker(subject) {
|
|
263
|
+
const text = subject === undefined || subject === null ? "" : String(subject).trim();
|
|
264
|
+
if (!text) return "New Message";
|
|
265
|
+
return text.length > MAIL_COMPOSE_TITLE_MARKER_MAX
|
|
266
|
+
? text.slice(0, MAIL_COMPOSE_TITLE_MARKER_MAX)
|
|
267
|
+
: text;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* JXA AX hit-test: `AXUIElementCopyElementAtPosition` at compose-body
|
|
272
|
+
* coordinates, then AXFocused + AXValue on the `AXWebArea` "message body".
|
|
273
|
+
* Mini-proven (computer-use): deep UI-element walks truncate before the
|
|
274
|
+
* WebArea; a coordinate hit-test does not. No mouse click.
|
|
275
|
+
*/
|
|
276
|
+
export function buildAtmAxHitTestJxa() {
|
|
277
|
+
return `ObjC.import("ApplicationServices");
|
|
278
|
+
ObjC.import("Foundation");
|
|
279
|
+
function axStr(el, attr) {
|
|
280
|
+
var out = Ref();
|
|
281
|
+
if ($.AXUIElementCopyAttributeValue(el, attr, out) !== 0) return "";
|
|
282
|
+
if (out[0] == null) return "";
|
|
283
|
+
try { return ObjC.unwrap(out[0]).toString(); } catch (e) { return ""; }
|
|
284
|
+
}
|
|
285
|
+
function axHeight(el) {
|
|
286
|
+
var out = Ref();
|
|
287
|
+
if ($.AXUIElementCopyAttributeValue(el, "AXSize", out) !== 0 || out[0] == null) return 0;
|
|
288
|
+
try {
|
|
289
|
+
var u = ObjC.unwrap(out[0]);
|
|
290
|
+
if (u && typeof u.height === "number") return u.height;
|
|
291
|
+
if (u && u.Height != null) return Number(u.Height);
|
|
292
|
+
if (u && u[1] != null) return Number(u[1]);
|
|
293
|
+
} catch (e) {}
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
function axBits(el) {
|
|
297
|
+
return [axStr(el, "AXDescription"), axStr(el, "AXTitle"), axStr(el, "AXRoleDescription")].join(" ");
|
|
298
|
+
}
|
|
299
|
+
function isHeaderHit(role, bits, height, minH) {
|
|
300
|
+
if (role === "AXTextField" || role === "AXComboBox" || role === "AXButton" || role === "AXMenuButton" || role === "AXPopUpButton" || role === "AXWindow") return true;
|
|
301
|
+
if (role === "AXTextArea" && height > 0 && height < minH) return true;
|
|
302
|
+
var lower = bits.toLowerCase();
|
|
303
|
+
if (lower.indexOf("to:") !== -1) return true;
|
|
304
|
+
if (lower.indexOf("cc:") !== -1) return true;
|
|
305
|
+
if (lower.indexOf("bcc:") !== -1) return true;
|
|
306
|
+
if (lower.indexOf("subject") !== -1 && lower.indexOf("message body") === -1) return true;
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
function isMailBodyHit(role, bits) {
|
|
310
|
+
if (role !== "AXWebArea") return false;
|
|
311
|
+
return bits.toLowerCase().indexOf("message body") !== -1;
|
|
312
|
+
}
|
|
313
|
+
function run(argv) {
|
|
314
|
+
// ${ATM_AX_HIT_TEST_MARKER}
|
|
315
|
+
var pid = parseInt(argv[0], 10);
|
|
316
|
+
var body = String(argv[1] || "");
|
|
317
|
+
var needle = String(argv[2] || "");
|
|
318
|
+
var pointStr = String(argv[3] || "");
|
|
319
|
+
var minH = parseInt(argv[4], 10);
|
|
320
|
+
if (!minH) minH = 50;
|
|
321
|
+
if (!pid) return "BODY_FOCUS_FAILED";
|
|
322
|
+
var app = $.AXUIElementCreateApplication(pid);
|
|
323
|
+
var points = pointStr.split(";");
|
|
324
|
+
var chosen = null;
|
|
325
|
+
for (var i = 0; i < points.length; i++) {
|
|
326
|
+
var xy = points[i].split(",");
|
|
327
|
+
var x = parseFloat(xy[0]);
|
|
328
|
+
var y = parseFloat(xy[1]);
|
|
329
|
+
if (!isFinite(x) || !isFinite(y)) continue;
|
|
330
|
+
var hit = Ref();
|
|
331
|
+
var err = $.AXUIElementCopyElementAtPosition(app, x, y, hit);
|
|
332
|
+
if (err !== 0 || hit[0] == null) continue;
|
|
333
|
+
var el = hit[0];
|
|
334
|
+
var role = axStr(el, "AXRole");
|
|
335
|
+
var bits = axBits(el);
|
|
336
|
+
var height = axHeight(el);
|
|
337
|
+
if (isHeaderHit(role, bits, height, minH)) continue;
|
|
338
|
+
if (isMailBodyHit(role, bits)) {
|
|
339
|
+
chosen = el;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!chosen) return "BODY_FOCUS_FAILED";
|
|
344
|
+
try { $.AXUIElementSetAttributeValue(chosen, "AXFocused", true); } catch (e) {}
|
|
345
|
+
try { $.AXUIElementSetAttributeValue(chosen, "AXValue", body); } catch (e) {}
|
|
346
|
+
var val = axStr(chosen, "AXValue");
|
|
347
|
+
if (val !== "" && needle !== "" && val.indexOf(needle) === -1) return "VALUE_MISS";
|
|
348
|
+
return "OK";
|
|
349
|
+
}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
251
352
|
export function isSystemEventsProbeScript(script) {
|
|
252
353
|
return String(script || "").includes(SYSTEM_EVENTS_PROBE_MARKER);
|
|
253
354
|
}
|
|
@@ -350,66 +451,34 @@ function systemEventsGrantKind(result) {
|
|
|
350
451
|
}
|
|
351
452
|
|
|
352
453
|
/**
|
|
353
|
-
* AppleScript handlers:
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
* `content of` (
|
|
360
|
-
*
|
|
454
|
+
* AppleScript handlers: find the compose window by subject title among
|
|
455
|
+
* top-level System Events UI elements (macOS 26 `windows of process Mail`
|
|
456
|
+
* is 0), then coordinate AX hit-test (`AXUIElementCopyElementAtPosition`)
|
|
457
|
+
* to the `AXWebArea` "message body". Mini computer-use: deep UI-element
|
|
458
|
+
* walks truncate after ~23 header/toolbar nodes and never reach the body;
|
|
459
|
+
* AXFocusedUIElement role coerce fails (-1700). No mouse click. No
|
|
460
|
+
* `entire contents`. Never live Mail `content of` (Mail 16 stays empty
|
|
461
|
+
* after UI fill). Never `set content` / `html content` (FB11734014).
|
|
361
462
|
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* events are wrapped in
|
|
463
|
+
* Hit must be AXWebArea "message body"; a short AXTextField is fail-closed
|
|
464
|
+
* (BODY_FOCUS_FAILED). Focus + AXValue (Mini: plain Sent .emlx, cite=0).
|
|
465
|
+
* Typed Returns / Cmd-V only if a readable AX value is still missing the
|
|
466
|
+
* needle. Header checks stay. System Events Apple events are wrapped in
|
|
467
|
+
* `with timeout`.
|
|
468
|
+
*
|
|
469
|
+
* Mini 2.1.3 compile: bare `value of attribute "AXTitle"` in
|
|
470
|
+
* atmTopElemBits / atmTopElemExactTitle is osacompile **-2741** (those
|
|
471
|
+
* handlers sit outside the System Events tell that walks top-level UI
|
|
472
|
+
* elements). 2.1.4 wraps those AXTitle reads inside
|
|
473
|
+
* `tell application "System Events"` (Mini solution-probe PASS).
|
|
366
474
|
*/
|
|
367
475
|
export function buildMailBodyPasteHandler() {
|
|
368
|
-
const tabMax = asInteger(MAIL_BODY_TAB_MAX, { min: 1, max: 12, field: "tabMax" });
|
|
369
476
|
const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
|
|
370
477
|
const bodyMinH = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
|
|
478
|
+
const topMax = asInteger(MAIL_COMPOSE_TOP_UI_MAX, { min: 1, max: 64, field: "topUiMax" });
|
|
479
|
+
const jxaLit = asString(buildAtmAxHitTestJxa());
|
|
371
480
|
return `${buildAtmFocusedElementHandler()}
|
|
372
481
|
|
|
373
|
-
on atmSafeToPaste()
|
|
374
|
-
tell application "System Events"
|
|
375
|
-
tell process "Mail"
|
|
376
|
-
try
|
|
377
|
-
set fe to my atmFocusedElement()
|
|
378
|
-
if fe is missing value then return false
|
|
379
|
-
set r to (role of fe) as string
|
|
380
|
-
if r is "AXWindow" then return false
|
|
381
|
-
if r is "AXTextField" then return false
|
|
382
|
-
if r is "AXComboBox" then return false
|
|
383
|
-
if r is "AXButton" then return false
|
|
384
|
-
if r is "AXMenuButton" then return false
|
|
385
|
-
if r is "AXPopUpButton" then return false
|
|
386
|
-
set atmH to 0
|
|
387
|
-
try
|
|
388
|
-
set {atmW, atmH} to size of fe
|
|
389
|
-
end try
|
|
390
|
-
if r is "AXTextArea" and atmH > 0 and atmH < ${bodyMinH} then return false
|
|
391
|
-
ignoring case
|
|
392
|
-
set bits to ""
|
|
393
|
-
try
|
|
394
|
-
set bits to bits & (description of fe as string) & " "
|
|
395
|
-
end try
|
|
396
|
-
try
|
|
397
|
-
set bits to bits & (name of fe as string) & " "
|
|
398
|
-
end try
|
|
399
|
-
if bits contains "To:" then return false
|
|
400
|
-
if bits contains "Cc:" then return false
|
|
401
|
-
if bits contains "Bcc:" then return false
|
|
402
|
-
if bits contains "Subject" then return false
|
|
403
|
-
end ignoring
|
|
404
|
-
if r is "AXWebArea" then return true
|
|
405
|
-
if r is "AXTextArea" then return true
|
|
406
|
-
if atmH >= ${bodyMinH} then return true
|
|
407
|
-
end try
|
|
408
|
-
return false
|
|
409
|
-
end tell
|
|
410
|
-
end tell
|
|
411
|
-
end atmSafeToPaste
|
|
412
|
-
|
|
413
482
|
on atmAxFocusedValue()
|
|
414
483
|
tell application "System Events"
|
|
415
484
|
try
|
|
@@ -421,6 +490,105 @@ on atmAxFocusedValue()
|
|
|
421
490
|
return ""
|
|
422
491
|
end atmAxFocusedValue
|
|
423
492
|
|
|
493
|
+
on atmTopElemBits(atmEl)
|
|
494
|
+
set atmBits to ""
|
|
495
|
+
try
|
|
496
|
+
set atmBits to atmBits & (name of atmEl as string) & " "
|
|
497
|
+
end try
|
|
498
|
+
try
|
|
499
|
+
set atmBits to atmBits & (title of atmEl as string) & " "
|
|
500
|
+
end try
|
|
501
|
+
try
|
|
502
|
+
set atmBits to atmBits & (description of atmEl as string) & " "
|
|
503
|
+
end try
|
|
504
|
+
try
|
|
505
|
+
tell application "System Events"
|
|
506
|
+
set atmBits to atmBits & (value of attribute "AXTitle" of atmEl as string) & " "
|
|
507
|
+
end tell
|
|
508
|
+
end try
|
|
509
|
+
return atmBits
|
|
510
|
+
end atmTopElemBits
|
|
511
|
+
|
|
512
|
+
on atmTopElemExactTitle(atmEl, atmMarker)
|
|
513
|
+
try
|
|
514
|
+
if (name of atmEl as string) is atmMarker then return true
|
|
515
|
+
end try
|
|
516
|
+
try
|
|
517
|
+
if (title of atmEl as string) is atmMarker then return true
|
|
518
|
+
end try
|
|
519
|
+
try
|
|
520
|
+
tell application "System Events"
|
|
521
|
+
if (value of attribute "AXTitle" of atmEl as string) is atmMarker then return true
|
|
522
|
+
end tell
|
|
523
|
+
end try
|
|
524
|
+
return false
|
|
525
|
+
end atmTopElemExactTitle
|
|
526
|
+
|
|
527
|
+
on atmComposeFrameByTitle(atmMarker)
|
|
528
|
+
with timeout of ${seSecs} seconds
|
|
529
|
+
tell application "System Events"
|
|
530
|
+
tell process "Mail"
|
|
531
|
+
set atmElems to UI elements
|
|
532
|
+
set atmCount to count of atmElems
|
|
533
|
+
if atmCount > ${topMax} then set atmCount to ${topMax}
|
|
534
|
+
repeat with atmIdx from 1 to atmCount
|
|
535
|
+
set atmEl to item atmIdx of atmElems
|
|
536
|
+
set atmRole to ""
|
|
537
|
+
try
|
|
538
|
+
set atmRole to (role of atmEl as string)
|
|
539
|
+
end try
|
|
540
|
+
if atmRole is not "AXMenuBar" then
|
|
541
|
+
if my atmTopElemExactTitle(atmEl, atmMarker) then
|
|
542
|
+
set atmPos to position of atmEl
|
|
543
|
+
set atmSz to size of atmEl
|
|
544
|
+
return {item 1 of atmPos, item 2 of atmPos, item 1 of atmSz, item 2 of atmSz}
|
|
545
|
+
end if
|
|
546
|
+
end if
|
|
547
|
+
end repeat
|
|
548
|
+
repeat with atmIdx from 1 to atmCount
|
|
549
|
+
set atmEl to item atmIdx of atmElems
|
|
550
|
+
set atmRole to ""
|
|
551
|
+
try
|
|
552
|
+
set atmRole to (role of atmEl as string)
|
|
553
|
+
end try
|
|
554
|
+
if atmRole is not "AXMenuBar" then
|
|
555
|
+
set atmBits to my atmTopElemBits(atmEl)
|
|
556
|
+
ignoring case
|
|
557
|
+
if atmBits contains atmMarker then
|
|
558
|
+
set atmPos to position of atmEl
|
|
559
|
+
set atmSz to size of atmEl
|
|
560
|
+
return {item 1 of atmPos, item 2 of atmPos, item 1 of atmSz, item 2 of atmSz}
|
|
561
|
+
end if
|
|
562
|
+
end ignoring
|
|
563
|
+
end if
|
|
564
|
+
end repeat
|
|
565
|
+
end tell
|
|
566
|
+
end tell
|
|
567
|
+
end timeout
|
|
568
|
+
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
569
|
+
end atmComposeFrameByTitle
|
|
570
|
+
|
|
571
|
+
on atmBodyHitPointList(atmFrameX, atmFrameY, atmFrameW, atmFrameH)
|
|
572
|
+
set atmMidX to (atmFrameX + (atmFrameW / 2)) as integer
|
|
573
|
+
set atmHitY1 to (atmFrameY + ((atmFrameH * 2) / 3)) as integer
|
|
574
|
+
set atmHitY2 to (atmFrameY + ((atmFrameH * 3) / 4)) as integer
|
|
575
|
+
set atmHitX2 to (atmFrameX + ((atmFrameW * 2) / 5)) as integer
|
|
576
|
+
set atmHitY3 to (atmFrameY + ((atmFrameH * 7) / 10)) as integer
|
|
577
|
+
return (atmMidX as string) & "," & (atmHitY1 as string) & ";" & (atmMidX as string) & "," & (atmHitY2 as string) & ";" & (atmHitX2 as string) & "," & (atmHitY3 as string)
|
|
578
|
+
end atmBodyHitPointList
|
|
579
|
+
|
|
580
|
+
on atmAxHitFill(atmPid, atmBody, atmNeedle, atmPoints)
|
|
581
|
+
set atmJxa to ${jxaLit}
|
|
582
|
+
set atmOut to ""
|
|
583
|
+
try
|
|
584
|
+
set atmOut to (run script atmJxa in "JavaScript" with parameters {atmPid, atmBody, atmNeedle, atmPoints, ${bodyMinH}}) as string
|
|
585
|
+
on error errMsg number errNum
|
|
586
|
+
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
587
|
+
error errMsg number errNum
|
|
588
|
+
end try
|
|
589
|
+
return atmOut
|
|
590
|
+
end atmAxHitFill
|
|
591
|
+
|
|
424
592
|
on atmSplitParagraphs(bodyText)
|
|
425
593
|
set atmSavedDelim to AppleScript's text item delimiters
|
|
426
594
|
set AppleScript's text item delimiters to return
|
|
@@ -459,30 +627,6 @@ on atmKeystrokeText(atmText)
|
|
|
459
627
|
end tell
|
|
460
628
|
end atmKeystrokeText
|
|
461
629
|
|
|
462
|
-
on atmTabIntoMailBody(msg)
|
|
463
|
-
tell application "Mail" to activate
|
|
464
|
-
delay 0.25
|
|
465
|
-
with timeout of ${seSecs} seconds
|
|
466
|
-
tell application "System Events"
|
|
467
|
-
tell process "Mail" to set frontmost to true
|
|
468
|
-
end tell
|
|
469
|
-
end timeout
|
|
470
|
-
delay 0.08
|
|
471
|
-
repeat with atmN from 0 to ${tabMax}
|
|
472
|
-
if my atmSafeToPaste() then return
|
|
473
|
-
if atmN is ${tabMax} then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
474
|
-
with timeout of ${seSecs} seconds
|
|
475
|
-
tell application "System Events"
|
|
476
|
-
tell process "Mail"
|
|
477
|
-
key code 48
|
|
478
|
-
end tell
|
|
479
|
-
end tell
|
|
480
|
-
end timeout
|
|
481
|
-
delay 0.08
|
|
482
|
-
end repeat
|
|
483
|
-
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
484
|
-
end atmTabIntoMailBody
|
|
485
|
-
|
|
486
630
|
on atmTypeMailBody(bodyText)
|
|
487
631
|
set atmLines to my atmSplitParagraphs(bodyText)
|
|
488
632
|
set atmLast to count of atmLines
|
|
@@ -527,17 +671,50 @@ on atmPasteMailBodyFallback(bodyText)
|
|
|
527
671
|
if hadClip then set the clipboard to savedClip
|
|
528
672
|
end atmPasteMailBodyFallback
|
|
529
673
|
|
|
530
|
-
on atmFillMailBody(bodyText, needle, msg)
|
|
674
|
+
on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
531
675
|
try
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
676
|
+
tell application "Mail" to activate
|
|
677
|
+
delay 0.25
|
|
678
|
+
with timeout of ${seSecs} seconds
|
|
679
|
+
tell application "System Events"
|
|
680
|
+
tell process "Mail" to set frontmost to true
|
|
681
|
+
end tell
|
|
682
|
+
end timeout
|
|
683
|
+
delay 0.08
|
|
684
|
+
set atmPid to 0
|
|
685
|
+
with timeout of ${seSecs} seconds
|
|
686
|
+
tell application "System Events"
|
|
687
|
+
tell process "Mail"
|
|
688
|
+
set atmPid to unix id
|
|
689
|
+
end tell
|
|
690
|
+
end tell
|
|
691
|
+
end timeout
|
|
692
|
+
if atmPid is 0 then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
693
|
+
set atmFrame to my atmComposeFrameByTitle(titleMarker)
|
|
694
|
+
set atmFrameX to item 1 of atmFrame
|
|
695
|
+
set atmFrameY to item 2 of atmFrame
|
|
696
|
+
set atmFrameW to item 3 of atmFrame
|
|
697
|
+
set atmFrameH to item 4 of atmFrame
|
|
698
|
+
if atmFrameW < 40 or atmFrameH < ${bodyMinH} then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
699
|
+
set atmPoints to my atmBodyHitPointList(atmFrameX, atmFrameY, atmFrameW, atmFrameH)
|
|
700
|
+
set atmHit to my atmAxHitFill(atmPid, bodyText, needle, atmPoints)
|
|
701
|
+
if atmHit is "${BODY_FOCUS_FAILED_SENTINEL}" then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
702
|
+
if atmHit is "VALUE_MISS" then
|
|
703
|
+
with timeout of ${seSecs} seconds
|
|
704
|
+
tell application "System Events"
|
|
705
|
+
tell process "Mail"
|
|
706
|
+
keystroke "a" using command down
|
|
707
|
+
end tell
|
|
708
|
+
end tell
|
|
709
|
+
end timeout
|
|
710
|
+
atmTypeMailBody(bodyText)
|
|
711
|
+
delay 0.12
|
|
712
|
+
if needle is not "" then
|
|
713
|
+
set atmAxVal to my atmAxFocusedValue()
|
|
714
|
+
if atmAxVal is not "" then
|
|
715
|
+
if atmAxVal does not contain needle then
|
|
716
|
+
atmPasteMailBodyFallback(bodyText)
|
|
717
|
+
end if
|
|
541
718
|
end if
|
|
542
719
|
end if
|
|
543
720
|
end if
|
|
@@ -591,13 +768,13 @@ ${bodyCheck}`;
|
|
|
591
768
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
592
769
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
593
770
|
* Recipients and subject are AppleScript properties (never paste). The body
|
|
594
|
-
* is
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
771
|
+
* is filled via title-marker compose window + coordinate AX hit-test to the
|
|
772
|
+
* AXWebArea "message body" (AXFocused + AXValue; Mini-proven plain Sent).
|
|
773
|
+
* Do not walk `windows of process Mail`. Do not deep-walk UI elements.
|
|
774
|
+
* Do not use Mail `content of` as the oracle that text landed (Mail 16 /
|
|
775
|
+
* macOS 26: that property stays empty). If the body element is not found or
|
|
776
|
+
* is a short AXTextField, or headers change, abort + delete (nothing sent).
|
|
777
|
+
* Ship prove that the body landed is Sent `.emlx` after send.
|
|
601
778
|
*/
|
|
602
779
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
603
780
|
const recipients = [
|
|
@@ -609,8 +786,9 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
|
|
|
609
786
|
const mailSecs = asInteger(MAIL_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 60, field: "mailTimeout" });
|
|
610
787
|
const plainBody = composeNativeBody(body, { html });
|
|
611
788
|
const needle = composeBodyNeedle(plainBody);
|
|
789
|
+
const titleMarker = composeTitleMarker(subject);
|
|
612
790
|
const pasteAndVerify = plainBody
|
|
613
|
-
? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, newMessage)
|
|
791
|
+
? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, ${asString(titleMarker)}, newMessage)
|
|
614
792
|
tell application "Mail"
|
|
615
793
|
${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
|
|
616
794
|
end tell`
|
package/lib/writeRouting.js
CHANGED
|
@@ -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, Calendar.app, and System Events (mail_send / mail_draft
|
|
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 fill the body via AX hit-test). " +
|
|
106
106
|
"Do not add node via + in the Contacts or Calendars privacy lists."
|
|
107
107
|
);
|
|
108
108
|
}
|