apple-tools-mcp 2.1.4 → 2.1.5
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 +3 -1
- package/lib/mailWrite.js +233 -225
- package/lib/writeTools.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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 paste the body into the compose window (exact subject title, role `AXWebArea`, proven focus, Cmd-A + clipboard). 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
|
|
|
@@ -486,6 +486,8 @@ Keystrokes use System Events, so **node needs Accessibility** (Privacy & Securit
|
|
|
486
486
|
|
|
487
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
488
|
|
|
489
|
+
**Probe 3 body delivery (2.1.5).** Mini Probe 3 (`atm-p3-deliver.applescript`, classic AppleScript / System Events, not JXA) proved a click-free path: exact subject window title, first `AXWebArea` from `entire contents` of that window, `set focused of` that element to true, then an independent `AXFocusedUIElement` re-read that must match role `AXWebArea` and position/size before any keystroke. Delivery is Cmd-A + clipboard paste. No Mail `content` / `html content`. No `AXValue` write (`AXUIElementSetAttributeValue` on content is a silent no-op; 2.1.4 `atmAxHitFill` returned `OK` when that value was empty, so the keystroke recovery never ran). No coordinate `click at` (Probe 3: a click can land on another Space or app). Focus mismatch is `BODY_FOCUS_UNPROVEN`: the draft is deleted and nothing is sent. The post-paste body needle runs only when the focused element is still that proven body, so a To: recipient chip (`U+FFFC`) is not `BODY_PASTE_MISDIRECTED`. Sent/Outbox verify, `dry_run`, and confirm are unchanged.
|
|
490
|
+
|
|
489
491
|
**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.
|
|
490
492
|
|
|
491
493
|
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.
|
package/lib/mailWrite.js
CHANGED
|
@@ -158,6 +158,10 @@ export function probeMailAutomation() {
|
|
|
158
158
|
export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
|
|
159
159
|
export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
160
160
|
export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
|
|
161
|
+
/** Focus request ran, but the re-read AXFocusedUIElement was not the chosen body. */
|
|
162
|
+
export const BODY_FOCUS_UNPROVEN_SENTINEL = "BODY_FOCUS_UNPROVEN";
|
|
163
|
+
/** Mail To: recipient chips surface as U+FFFC (object replacement), not body text. */
|
|
164
|
+
export const MAIL_RECIPIENT_TOKEN_CHAR_ID = 65532;
|
|
161
165
|
export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
|
|
162
166
|
export const SYSTEM_EVENTS_PROBE_MARKER = "ATM_SYSTEM_EVENTS_PROBE";
|
|
163
167
|
/** Permissions-only marker: process / GUI scripting, not application `name`. */
|
|
@@ -268,10 +272,11 @@ export function composeTitleMarker(subject) {
|
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
/**
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
+
* Optional coordinate finder kept off the compose delivery path.
|
|
276
|
+
* 2.1.3/2.1.4 called this and treated an empty AXValue as "OK", so the
|
|
277
|
+
* keystroke recovery never ran (Probe 2: AXValue writes are a silent no-op).
|
|
278
|
+
* Probe 3 delivery does not use it: no AXUIElementSetAttributeValue, no
|
|
279
|
+
* click, and an empty value is not success.
|
|
275
280
|
*/
|
|
276
281
|
export function buildAtmAxHitTestJxa() {
|
|
277
282
|
return `ObjC.import("ApplicationServices");
|
|
@@ -313,11 +318,9 @@ function isMailBodyHit(role, bits) {
|
|
|
313
318
|
function run(argv) {
|
|
314
319
|
// ${ATM_AX_HIT_TEST_MARKER}
|
|
315
320
|
var pid = parseInt(argv[0], 10);
|
|
316
|
-
var
|
|
317
|
-
var
|
|
318
|
-
|
|
319
|
-
var minH = parseInt(argv[4], 10);
|
|
320
|
-
if (!minH) minH = 50;
|
|
321
|
+
var pointStr = String(argv[1] || "");
|
|
322
|
+
var minH = parseInt(argv[2], 10);
|
|
323
|
+
if (!minH) minH = ${MAIL_BODY_MIN_AX_HEIGHT};
|
|
321
324
|
if (!pid) return "BODY_FOCUS_FAILED";
|
|
322
325
|
var app = $.AXUIElementCreateApplication(pid);
|
|
323
326
|
var points = pointStr.split(";");
|
|
@@ -341,11 +344,7 @@ function run(argv) {
|
|
|
341
344
|
}
|
|
342
345
|
}
|
|
343
346
|
if (!chosen) return "BODY_FOCUS_FAILED";
|
|
344
|
-
|
|
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";
|
|
347
|
+
return "FOUND";
|
|
349
348
|
}`;
|
|
350
349
|
}
|
|
351
350
|
|
|
@@ -451,33 +450,29 @@ function systemEventsGrantKind(result) {
|
|
|
451
450
|
}
|
|
452
451
|
|
|
453
452
|
/**
|
|
454
|
-
* AppleScript handlers:
|
|
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).
|
|
453
|
+
* AppleScript handlers: Probe 3 body delivery (Mini PASS, classic AppleScript).
|
|
462
454
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
* `
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* `tell application "System Events"` (Mini solution-probe PASS).
|
|
455
|
+
* Find the compose window by exact subject title (`name of` windows, then
|
|
456
|
+
* the 2.1.4 top-level exact-title fallback when `windows` is empty). Find
|
|
457
|
+
* the body by walking `entire contents` of that window and taking the first
|
|
458
|
+
* `AXWebArea`. Focus with System Events `set focused of <AXWebArea> to true`
|
|
459
|
+
* — not `set value`, not `AXUIElementSetAttributeValue`. Re-read process
|
|
460
|
+
* `AXFocusedUIElement` and require role `AXWebArea` plus the same position
|
|
461
|
+
* and size. Mismatch is `BODY_FOCUS_UNPROVEN` (draft deleted, nothing sent).
|
|
462
|
+
* Deliver with Cmd-A and clipboard paste. No coordinate click. No Mail
|
|
463
|
+
* `content` / `html content`. AXTitle reads in the title helpers stay inside
|
|
464
|
+
* `tell application "System Events"` (2.1.4 osacompile -2741).
|
|
474
465
|
*/
|
|
475
466
|
export function buildMailBodyPasteHandler() {
|
|
476
467
|
const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
|
|
477
|
-
const bodyMinH = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
|
|
478
468
|
const topMax = asInteger(MAIL_COMPOSE_TOP_UI_MAX, { min: 1, max: 64, field: "topUiMax" });
|
|
479
|
-
const
|
|
480
|
-
return
|
|
469
|
+
const tokenId = asInteger(MAIL_RECIPIENT_TOKEN_CHAR_ID, { min: 1, max: 1114111, field: "tokenCharId" });
|
|
470
|
+
return `property atmProvenBx : -1
|
|
471
|
+
property atmProvenBy : -1
|
|
472
|
+
property atmProvenBw : -1
|
|
473
|
+
property atmProvenBh : -1
|
|
474
|
+
|
|
475
|
+
${buildAtmFocusedElementHandler()}
|
|
481
476
|
|
|
482
477
|
on atmAxFocusedValue()
|
|
483
478
|
tell application "System Events"
|
|
@@ -524,200 +519,197 @@ on atmTopElemExactTitle(atmEl, atmMarker)
|
|
|
524
519
|
return false
|
|
525
520
|
end atmTopElemExactTitle
|
|
526
521
|
|
|
527
|
-
on
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
592
|
-
on atmSplitParagraphs(bodyText)
|
|
593
|
-
set atmSavedDelim to AppleScript's text item delimiters
|
|
594
|
-
set AppleScript's text item delimiters to return
|
|
595
|
-
set atmCrParts to text items of bodyText
|
|
596
|
-
set AppleScript's text item delimiters to linefeed
|
|
597
|
-
set atmNorm to atmCrParts as string
|
|
598
|
-
set atmLines to text items of atmNorm
|
|
599
|
-
set AppleScript's text item delimiters to atmSavedDelim
|
|
600
|
-
return atmLines
|
|
601
|
-
end atmSplitParagraphs
|
|
602
|
-
|
|
603
|
-
on atmSpacesForTabs(atmLine)
|
|
604
|
-
set atmSavedDelim to AppleScript's text item delimiters
|
|
605
|
-
set AppleScript's text item delimiters to tab
|
|
606
|
-
set atmTabParts to text items of atmLine
|
|
607
|
-
set AppleScript's text item delimiters to " "
|
|
608
|
-
set atmOut to atmTabParts as string
|
|
609
|
-
set AppleScript's text item delimiters to atmSavedDelim
|
|
610
|
-
return atmOut
|
|
611
|
-
end atmSpacesForTabs
|
|
612
|
-
|
|
613
|
-
on atmKeystrokeText(atmText)
|
|
614
|
-
set atmSafe to my atmSpacesForTabs(atmText)
|
|
615
|
-
set atmLen to length of atmSafe
|
|
616
|
-
if atmLen is 0 then return
|
|
617
|
-
set atmPos to 1
|
|
522
|
+
on atmFocusedIsProvenBody()
|
|
523
|
+
set atmWantBx to my atmProvenBx
|
|
524
|
+
set atmWantBy to my atmProvenBy
|
|
525
|
+
set atmWantBw to my atmProvenBw
|
|
526
|
+
set atmWantBh to my atmProvenBh
|
|
527
|
+
if atmWantBw is -1 or atmWantBh is -1 then return false
|
|
618
528
|
tell application "System Events"
|
|
619
529
|
tell process "Mail"
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
530
|
+
set atmFocusedElem to missing value
|
|
531
|
+
try
|
|
532
|
+
set atmFocusedElem to value of attribute "AXFocusedUIElement"
|
|
533
|
+
end try
|
|
534
|
+
if atmFocusedElem is missing value then return false
|
|
535
|
+
set atmFocRole to ""
|
|
536
|
+
set atmFx to -1
|
|
537
|
+
set atmFy to -1
|
|
538
|
+
set atmFw to -1
|
|
539
|
+
set atmFh to -1
|
|
540
|
+
try
|
|
541
|
+
set atmFocRole to (role of atmFocusedElem as string)
|
|
542
|
+
set atmFocPos to position of atmFocusedElem
|
|
543
|
+
set atmFocSz to size of atmFocusedElem
|
|
544
|
+
set atmFx to item 1 of atmFocPos
|
|
545
|
+
set atmFy to item 2 of atmFocPos
|
|
546
|
+
set atmFw to item 1 of atmFocSz
|
|
547
|
+
set atmFh to item 2 of atmFocSz
|
|
548
|
+
end try
|
|
549
|
+
if atmFocRole is not "AXWebArea" then return false
|
|
550
|
+
if atmFx is not atmWantBx then return false
|
|
551
|
+
if atmFy is not atmWantBy then return false
|
|
552
|
+
if atmFw is not atmWantBw then return false
|
|
553
|
+
if atmFh is not atmWantBh then return false
|
|
554
|
+
return true
|
|
626
555
|
end tell
|
|
627
556
|
end tell
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
set
|
|
651
|
-
set
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
set
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
tell process "Mail"
|
|
661
|
-
keystroke "v" using command down
|
|
662
|
-
end tell
|
|
663
|
-
end tell
|
|
664
|
-
end timeout
|
|
665
|
-
delay 0.15
|
|
666
|
-
on error errMsg number errNum
|
|
667
|
-
if hadClip then set the clipboard to savedClip
|
|
668
|
-
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
669
|
-
error errMsg number errNum
|
|
670
|
-
end try
|
|
671
|
-
if hadClip then set the clipboard to savedClip
|
|
672
|
-
end atmPasteMailBodyFallback
|
|
557
|
+
return false
|
|
558
|
+
end atmFocusedIsProvenBody
|
|
559
|
+
|
|
560
|
+
on atmAxValueIsRecipientToken(atmVal)
|
|
561
|
+
if atmVal is "" then return false
|
|
562
|
+
set atmTok to character id ${tokenId}
|
|
563
|
+
if atmVal does not contain atmTok then return false
|
|
564
|
+
set atmSaved to AppleScript's text item delimiters
|
|
565
|
+
set AppleScript's text item delimiters to atmTok
|
|
566
|
+
set atmParts to text items of atmVal
|
|
567
|
+
set AppleScript's text item delimiters to ""
|
|
568
|
+
set atmRest to atmParts as string
|
|
569
|
+
set AppleScript's text item delimiters to " "
|
|
570
|
+
set atmSpaceParts to text items of atmRest
|
|
571
|
+
set AppleScript's text item delimiters to ""
|
|
572
|
+
set atmRest to atmSpaceParts as string
|
|
573
|
+
set AppleScript's text item delimiters to return
|
|
574
|
+
set atmLineParts to text items of atmRest
|
|
575
|
+
set AppleScript's text item delimiters to ""
|
|
576
|
+
set atmRest to atmLineParts as string
|
|
577
|
+
set AppleScript's text item delimiters to linefeed
|
|
578
|
+
set atmLfParts to text items of atmRest
|
|
579
|
+
set AppleScript's text item delimiters to ""
|
|
580
|
+
set atmRest to atmLfParts as string
|
|
581
|
+
set AppleScript's text item delimiters to tab
|
|
582
|
+
set atmTabParts to text items of atmRest
|
|
583
|
+
set AppleScript's text item delimiters to ""
|
|
584
|
+
set atmRest to atmTabParts as string
|
|
585
|
+
set AppleScript's text item delimiters to atmSaved
|
|
586
|
+
if atmRest is "" then return true
|
|
587
|
+
return false
|
|
588
|
+
end atmAxValueIsRecipientToken
|
|
673
589
|
|
|
674
590
|
on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
675
591
|
try
|
|
676
592
|
tell application "Mail" to activate
|
|
677
593
|
delay 0.25
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
end timeout
|
|
683
|
-
delay 0.08
|
|
684
|
-
set atmPid to 0
|
|
594
|
+
set atmSaveBx to -1
|
|
595
|
+
set atmSaveBy to -1
|
|
596
|
+
set atmSaveBw to -1
|
|
597
|
+
set atmSaveBh to -1
|
|
685
598
|
with timeout of ${seSecs} seconds
|
|
686
599
|
tell application "System Events"
|
|
687
600
|
tell process "Mail"
|
|
688
|
-
set
|
|
601
|
+
set frontmost to true
|
|
602
|
+
delay 0.3
|
|
603
|
+
set atmWin to missing value
|
|
604
|
+
repeat with atmCand in windows
|
|
605
|
+
try
|
|
606
|
+
if (name of atmCand as string) is titleMarker then
|
|
607
|
+
set atmWin to atmCand
|
|
608
|
+
exit repeat
|
|
609
|
+
end if
|
|
610
|
+
end try
|
|
611
|
+
end repeat
|
|
612
|
+
if atmWin is missing value then
|
|
613
|
+
set atmElems to UI elements
|
|
614
|
+
set atmCount to count of atmElems
|
|
615
|
+
if atmCount > ${topMax} then set atmCount to ${topMax}
|
|
616
|
+
repeat with atmIdx from 1 to atmCount
|
|
617
|
+
set atmEl to item atmIdx of atmElems
|
|
618
|
+
set atmRole to ""
|
|
619
|
+
try
|
|
620
|
+
set atmRole to (role of atmEl as string)
|
|
621
|
+
end try
|
|
622
|
+
if atmRole is not "AXMenuBar" then
|
|
623
|
+
if my atmTopElemExactTitle(atmEl, titleMarker) then
|
|
624
|
+
set atmWin to atmEl
|
|
625
|
+
exit repeat
|
|
626
|
+
end if
|
|
627
|
+
end if
|
|
628
|
+
end repeat
|
|
629
|
+
end if
|
|
630
|
+
if atmWin is missing value then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
631
|
+
try
|
|
632
|
+
perform action "AXRaise" of atmWin
|
|
633
|
+
end try
|
|
634
|
+
delay 0.3
|
|
635
|
+
set atmBody to missing value
|
|
636
|
+
set atmAll to {}
|
|
637
|
+
try
|
|
638
|
+
set atmAll to entire contents of atmWin
|
|
639
|
+
end try
|
|
640
|
+
repeat with atmEl in atmAll
|
|
641
|
+
try
|
|
642
|
+
if (role of atmEl as string) is "AXWebArea" then
|
|
643
|
+
set atmBody to atmEl
|
|
644
|
+
exit repeat
|
|
645
|
+
end if
|
|
646
|
+
end try
|
|
647
|
+
end repeat
|
|
648
|
+
if atmBody is missing value then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
649
|
+
set atmBodyPos to position of atmBody
|
|
650
|
+
set atmBodySz to size of atmBody
|
|
651
|
+
set atmBx to item 1 of atmBodyPos
|
|
652
|
+
set atmBy to item 2 of atmBodyPos
|
|
653
|
+
set atmBw to item 1 of atmBodySz
|
|
654
|
+
set atmBh to item 2 of atmBodySz
|
|
655
|
+
try
|
|
656
|
+
set focused of atmBody to true
|
|
657
|
+
end try
|
|
658
|
+
delay 0.3
|
|
659
|
+
set atmFocusedElem to missing value
|
|
660
|
+
try
|
|
661
|
+
set atmFocusedElem to value of attribute "AXFocusedUIElement"
|
|
662
|
+
end try
|
|
663
|
+
if atmFocusedElem is missing value then error "${BODY_FOCUS_UNPROVEN_SENTINEL}"
|
|
664
|
+
set atmFocRole to ""
|
|
665
|
+
set atmFx to -1
|
|
666
|
+
set atmFy to -1
|
|
667
|
+
set atmFw to -1
|
|
668
|
+
set atmFh to -1
|
|
669
|
+
try
|
|
670
|
+
set atmFocRole to (role of atmFocusedElem as string)
|
|
671
|
+
set atmFocPos to position of atmFocusedElem
|
|
672
|
+
set atmFocSz to size of atmFocusedElem
|
|
673
|
+
set atmFx to item 1 of atmFocPos
|
|
674
|
+
set atmFy to item 2 of atmFocPos
|
|
675
|
+
set atmFw to item 1 of atmFocSz
|
|
676
|
+
set atmFh to item 2 of atmFocSz
|
|
677
|
+
end try
|
|
678
|
+
if atmFocRole is not "AXWebArea" then error "${BODY_FOCUS_UNPROVEN_SENTINEL}"
|
|
679
|
+
if atmFx is not atmBx then error "${BODY_FOCUS_UNPROVEN_SENTINEL}"
|
|
680
|
+
if atmFy is not atmBy then error "${BODY_FOCUS_UNPROVEN_SENTINEL}"
|
|
681
|
+
if atmFw is not atmBw then error "${BODY_FOCUS_UNPROVEN_SENTINEL}"
|
|
682
|
+
if atmFh is not atmBh then error "${BODY_FOCUS_UNPROVEN_SENTINEL}"
|
|
683
|
+
set atmSaveBx to atmBx
|
|
684
|
+
set atmSaveBy to atmBy
|
|
685
|
+
set atmSaveBw to atmBw
|
|
686
|
+
set atmSaveBh to atmBh
|
|
687
|
+
set savedClip to ""
|
|
688
|
+
set hadClip to false
|
|
689
|
+
try
|
|
690
|
+
set savedClip to the clipboard as text
|
|
691
|
+
set hadClip to true
|
|
692
|
+
end try
|
|
693
|
+
try
|
|
694
|
+
keystroke "a" using command down
|
|
695
|
+
delay 0.2
|
|
696
|
+
set the clipboard to bodyText
|
|
697
|
+
delay 0.2
|
|
698
|
+
keystroke "v" using command down
|
|
699
|
+
delay 0.6
|
|
700
|
+
on error errMsg number errNum
|
|
701
|
+
if hadClip then set the clipboard to savedClip
|
|
702
|
+
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
703
|
+
error errMsg number errNum
|
|
704
|
+
end try
|
|
705
|
+
if hadClip then set the clipboard to savedClip
|
|
689
706
|
end tell
|
|
690
707
|
end tell
|
|
691
708
|
end timeout
|
|
692
|
-
|
|
693
|
-
set
|
|
694
|
-
set
|
|
695
|
-
set
|
|
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
|
|
718
|
-
end if
|
|
719
|
-
end if
|
|
720
|
-
end if
|
|
709
|
+
set my atmProvenBx to atmSaveBx
|
|
710
|
+
set my atmProvenBy to atmSaveBy
|
|
711
|
+
set my atmProvenBw to atmSaveBw
|
|
712
|
+
set my atmProvenBh to atmSaveBh
|
|
721
713
|
on error errMsg number errNum
|
|
722
714
|
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
723
715
|
if errMsg contains "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
@@ -726,6 +718,7 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
|
726
718
|
end atmFillMailBody`;
|
|
727
719
|
}
|
|
728
720
|
|
|
721
|
+
|
|
729
722
|
export const buildMailBodyFillHandler = buildMailBodyPasteHandler;
|
|
730
723
|
|
|
731
724
|
function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
@@ -745,9 +738,13 @@ function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
|
745
738
|
})
|
|
746
739
|
.join("\n");
|
|
747
740
|
const bodyCheck = needle
|
|
748
|
-
? `
|
|
749
|
-
|
|
750
|
-
if atmAxVal
|
|
741
|
+
? ` if my atmFocusedIsProvenBody() then
|
|
742
|
+
set atmAxVal to my atmAxFocusedValue()
|
|
743
|
+
if atmAxVal is not "" then
|
|
744
|
+
if my atmAxValueIsRecipientToken(atmAxVal) is false then
|
|
745
|
+
if atmAxVal does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
746
|
+
end if
|
|
747
|
+
end if
|
|
751
748
|
end if`
|
|
752
749
|
: "";
|
|
753
750
|
return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
@@ -768,13 +765,16 @@ ${bodyCheck}`;
|
|
|
768
765
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
769
766
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
770
767
|
* Recipients and subject are AppleScript properties (never paste). The body
|
|
771
|
-
* is filled
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
* macOS 26
|
|
776
|
-
* is a
|
|
777
|
-
*
|
|
768
|
+
* is filled by Probe 3: exact subject title, first AXWebArea under that
|
|
769
|
+
* window (`entire contents`), `set focused of` that element, prove focus
|
|
770
|
+
* (role + position + size) via a fresh AXFocusedUIElement read, then Cmd-A
|
|
771
|
+
* and clipboard paste. No coordinate click. No AXValue write. No Mail
|
|
772
|
+
* `content of` oracle (Mail 16 / macOS 26 stays empty). Missing window or
|
|
773
|
+
* body is BODY_FOCUS_FAILED; a focus-proof mismatch is BODY_FOCUS_UNPROVEN.
|
|
774
|
+
* Either deletes the draft and sends nothing. Header checks stay. The body
|
|
775
|
+
* needle runs only when the focused element is still that proven body, so a
|
|
776
|
+
* To: U+FFFC chip is not BODY_PASTE_MISDIRECTED. Ship prove that the body
|
|
777
|
+
* landed is Sent `.emlx` after send.
|
|
778
778
|
*/
|
|
779
779
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
780
780
|
const recipients = [
|
|
@@ -1199,6 +1199,11 @@ export function isMailBodyFocusFailed(result) {
|
|
|
1199
1199
|
return String(result.error || "").toLowerCase().includes("body_focus_failed");
|
|
1200
1200
|
}
|
|
1201
1201
|
|
|
1202
|
+
export function isMailBodyFocusUnproven(result) {
|
|
1203
|
+
if (!result) return false;
|
|
1204
|
+
return String(result.error || "").toLowerCase().includes("body_focus_unproven");
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1202
1207
|
export function isMailBodyPasteMisdirected(result) {
|
|
1203
1208
|
if (!result) return false;
|
|
1204
1209
|
const text = String(result.error || "").toLowerCase();
|
|
@@ -1210,6 +1215,9 @@ function failure(action, summary, result, secrets) {
|
|
|
1210
1215
|
if (err.includes("gui_scripting_unavailable")) {
|
|
1211
1216
|
return `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
|
|
1212
1217
|
}
|
|
1218
|
+
if (err.includes("body_focus_unproven")) {
|
|
1219
|
+
return `${action} failed — attempted to ${summary}. Keyboard focus was not on the message body (focus proof failed); the draft was discarded and nothing was sent.`;
|
|
1220
|
+
}
|
|
1213
1221
|
if (err.includes("body_focus_failed")) {
|
|
1214
1222
|
return `${action} failed — attempted to ${summary}. The compose caret could not be moved into the message body; nothing was sent.`;
|
|
1215
1223
|
}
|
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; the body is
|
|
60
|
+
body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped; the body is pasted into Mail's compose window (System Events focus on the message body, then Cmd-A and clipboard paste). 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; the body is
|
|
77
|
+
body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped; the body is pasted into Mail's compose window (System Events focus on the message body, then Cmd-A and clipboard paste). AppleScript content/html content is not set because it quote-wraps the Sent body" },
|
|
78
78
|
...CONFIRM_PROPS
|
|
79
79
|
},
|
|
80
80
|
required: ["to"]
|