Repository navigation
feat!: parse create like update, give every refusal one error shape, and narrow the library API - #26
Merged
Merged
Conversation
Create builds its request from parseStringField, parseDateField, parseURLField, parseRecurrenceField and parseAlarmsField, so a wrong-typed notes, location, dueDate or url fails the item instead of being dropped, and `done` on create is honoured. The tool-layer relative-alarm pre-check goes; the service's check and message apply. Wall-clock absoluteDate values anchor to startTimeZone, else dueTimeZone, like every other date input. parseDate, requireDateWithTimeInfo and parseURL are gone. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
Parameter errors join ParseError, the read-only refusal and the unknown-tool error get a small ToolCallError, and the single catch in handleToolCall builds every error result. Every refusal now reads "Error: ..."; before, eight early returns carried no prefix. A non-string element in write_reminders' delete array now fails that item as delete[i], like a malformed upsert element, instead of rejecting the call. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
Creating a reminder without listId, when the default list is outside --allowed-lists, threw listAccessDenied with the default list's identifier: an ID the server looked up, not one the caller supplied. The new argument-free defaultListNotAllowed says "The default reminder list is outside --allowed-lists; pass listId". The README now states what the allowlist does to callers. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
…null The output schemas now carry the enums the values already came from: alarm kind, reminder priority and the manage_reminder_list action. The bytes are unchanged; ReminderPriorityInput(_:) replaces displayName.lowercased(). notes, dueDate, location and url now say "Set to null to remove", like the other three clearable fields, and FailureOutput.id says what it holds. A test pins that every clearable field's advertised schema accepts null. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
Removed from EventKitService, none of which the server called: - the Reminder and ReminderList typealiases; - ReminderListModel.reminderCount and ReminderModel.creationDate and lastModifiedDate, which nothing rendered; - CaseIterable on ReminderPriority, and ListAccessPolicy.filter; - getList(id:) and getReminder(id:), from the protocol and the actor. validateAllowedLists() now throws. It used to answer a busy event store with an unrestricted, non-fatal result, so startup logged a restriction to zero lists and served anyway; now the server exits, as it does when access is denied. AllowedListValidation.unrestricted existed only for that fallback. A test holds an injected operation gate and expects the timeout. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
EventKit restamps completionDate whenever isCompleted is set to true, so sending done: true for a reminder that was already done moved its completion date to now. The update now writes isCompleted only when it changes. The manual stamp for a missing completionDate is gone: EventKit always sets one. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
…pletion date in the smoke checklist Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
- An absolute alarm resolves the item's zone only when it needs one. A stray dueTimeZone beside a location or relative alarm no longer fails the update, as it did not before this branch. The absoluteDate description now says a time without an offset is read in startTimeZone, else dueTimeZone. - ReminderAlarmModel.Kind and .kind lose their last use and are removed. - Tests pin a non-string startDate on create, a zone key without its date on create, and the non-object upsert element's message. A stale comment about completion stamping now matches the fix. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
- List colours were written as Generic RGB, so the store shifted them: #FF5733 read back as #FF6F41, in v2.0.1 as well. Colours are now written as sRGB, and read back converted to sRGB and rounded. - The store saves cleared notes as "", so notes cleared with null read back as an empty string. Empty notes now map to none. - A write batch where every item failed began with a blank summary line. It now says "No changes made.", as an empty batch already did. - The README says what Reminders does on save: a timed due date gets a matching start date, a start date without a due date loses its zone, and completing a recurring reminder splits off the done occurrence. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
… location Reminders never stores the plain location text. The smoke test showed it read back as nil after every save, through the server and through plain EventKit, in v2.0.1 too. A location set in Reminders.app is saved purely as a location alarm, whose title is the place. The write input, the reminder output and the library's request and model types lose `location`. The alarms description and the README now point to location alarms instead. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
The server required a start date for relative alarms and documented them as counting back from it. Reminders.app counts them from the due date: a 30-minute alarm on a reminder due 10:00 and starting 08:00 shows at 9:30, and one with only a start date shows no time at all. A relative alarm now needs a due date. relativeAlarmRequiresStartDate becomes relativeAlarmRequiresDueDate, "Relative alarms require a due date", and the text output says "15 min before due" and "at due time". The smoke checklist gains this step, splits the done and recurring round trips, and records the 2026-10-07 run. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
The second adversarial review found that an update sending dueDate: null without alarms kept the reminder's relative alarms, leaving the no-time state that create now refuses. The update now throws relativeAlarmRequiresDueDate before changing anything; clearing the due date together with the alarms, or once only absolute and location alarms remain, still works. The contract test's valid write example pairs its relative alarm with a due date. Claude-Session: https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu
|
🎉 This PR is included in version 3.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The deliberate major release of the refactoring series. Merging this cuts v3.0.0 through the
!in the title. Every change a client or a library consumer can observe is listed here. Review it commit by commit.Observable changes for MCP clients
Create now reads input exactly like update
notes,dueDateorstartDateon create fails that item. Before, create dropped the field silently. The advertised schema already forbade these values.dueTimeZoneandstartTimeZoneonly together with their date, as update always has. Before, create rejected an invalid zone sent without its date. It now ignores that zone.doneon create is honoured. The schema advertised it, but create ignored it.absoluteDateis read instartTimeZone, elsedueTimeZone. Before, it used the server's zone. The zone is resolved only for an absolute alarm, so a stray zone beside other alarm kinds is not checked.Fields now match what Reminders stores (found by the smoke test against a real database)
locationtext field is gone from the write input and the reminder output. Reminders never stored it: it read back empty after every save, in v2.0.1 too, and a location set in Reminders.app is stored only as a location alarm. The alarms description and the README now point to location alarms.#FF5733came back as#FF6F41, in v2.0.1 too.Errors have one shape
Error:. This now includes the read-only refusal, an unknown tool, and missing or invalid parameters. Before, eight early returns had no prefix.deleteelement fails only that item, reported asdelete[i]. Before, it rejected the whole call. A non-objectupsertelement now reads "Invalid item format: expected an object" instead of "Invalid item format".listId, when the default list is outside--allowed-lists, fails with "The default reminder list is outside --allowed-lists; pass listId". Before, the message carried the default list's identifier, which the server had looked up.Startup and data
--allowed-lists. Before, a busy event store made the startup check pass as "unrestricted", and the server served anyway.isCompleted = true, so the server now writes the flag only when it changes.Schemas
kind, reminderpriorityand the listactionin the output schemas now carry the enum their values already came from. The bytes on the wire are unchanged.notes,dueDateandurlnow say "Set to null to remove", like the other clearable fields.FailureOutput.idsays what it holds,absoluteDatesays which zone a time without an offset uses, and the relative-alarm descriptions name the due date.This PR's diff of the golden contract,
Tests/EventKitMCPTests/Contract/tool-contract.json, shows items 5, 6, 15 and 16 and nothing else.Library API changes
swift package diagnose-api-breaking-changes v2.0.1 --products EventKitServicereports exactly these:The
locationmembers and everything else removed were unused by the server, or never stored by Reminders.CreateReminderRequest.initgained a defaulteddone:parameter, so labelled call sites still compile oncelocation:is dropped.defaultListNotAllowedandrelativeAlarmRequiresDueDateare new cases, so an exhaustiveswitchoverReminderServiceErrorneeds updating.Smoke test
docs/SMOKE_TEST.mdran on 2026-10-07 against the real Reminders database on macOS 26.6.2, in a scratch list that was deleted afterwards. The release build, the tool surface, the allowlist, the round trip, the zones, the completion date, relative alarms, the coloured list and the overview header all pass on the final build. Steps 2 and 11 need another account and a Claude Desktop restart, and were not run.The run found the problems behind items 5 to 8 and 11, all of which shipped in v2.0.1. It also confirmed three EventKit behaviours, now in the README and the checklist:
Verification
swift test: 95 library tests and 114 server tests pass.alarmsout kept its relative alarms. It is now refused before anything changes, and a test pins it.Mutation record
Each mutation was reverted before committing.
noteswithstringValueagainstartDateagaindone: falsedoneon createdueTimeZonewithout its dateError:prefix in the single catchlistAccessDeniedwith the default list's ID againnotesnon-nullable in the input schemaisCompletedeven when unchangedThe
locationremoval adds no guard, so it has no mutation. The golden diff and the API diff are its proof.Release impact
Major: v3.0.0. The tap bump follows automatically.
https://claude.ai/code/session_01XR9wyQPT1iM8wzP9hvmYRu