• v0.16.0 2580d06307

    v0.16.0 — guard the permanent delete
    All checks were successful
    CI / gate (push) Successful in 1m18s
    CI / image (push) Successful in 50s
    Stable

    joeyr released this 2026-08-12 12:28:12 -05:00 | 54 commits to main since this release

    Two corrections to mail's tool surface, both found by reading it against the calendar's
    rather than by anything failing.

    Added

    • delete_message(permanent=true) now requires expected_subject, checked against what
      the handle actually resolves to, and refuses naming both subjects on a mismatch.

      Every other destructive tool in this project already worked this way — delete_event
      requires expected_title, delete_calendar requires expected_name and an event count —
      and mail's was the only one taking a handle alone. It was also the worst place for the
      omission: a deleted calendar event can be rebuilt from a listing you already have, while
      an expunged message is gone from Apple's servers with no trash, no device copy and no
      recovery. The most dangerous operation here was the least guarded.

      The default delete is deliberately left alone. It lands in trash and returns the
      handle to undo it, so demanding a subject every time would be friction with nothing behind
      it. Guard the irreversible branch, keep the reversible one cheap.

      Comparison is stripped and case-folded, matching delete_event: a caller echoing a
      subject back through a model does not reproduce whitespace exactly, and refusing on a
      trailing space teaches it to stop passing the argument. The refusal is raised before
      anything is flagged, and the tool description says that a mismatch means the wrong message
      was addressed — so re-read the folder rather than retrying with the subject from the error,
      which would delete precisely the message the guard just saved.

    Changed

    • get_unread_countlist_unread_counts. It answers with a collection of folders,
      and get_ is reserved for fetching exactly one named thing. The old name promised a
      number and returned every folder on the account. The optional folder argument stays, and
      now reads the same way list_messages(folder=…) does — a list narrowed by a filter, not a
      lookup.

      The rule this settles, now written in CLAUDE.md: get_ returns one named thing,
      list_ returns a collection.
      The rest of the surface already obeyed it.

    Downloads