ales27pm's picture
Add autonomous on-device iOS assistant
7b2dfc5 verified
Raw
History Blame Contribute Delete
2.72 kB
import EventKit
import Foundation
/// The production adapter is intentionally read-only and non-interactive.
/// Permission prompts belong to an explicit Settings flow, never tool
/// execution, so an agent run cannot surprise the user with a system prompt.
actor EventKitCalendarReader: SystemCalendarReading {
private let eventStore: EKEventStore
init(eventStore: EKEventStore = EKEventStore()) {
self.eventStore = eventStore
}
func authorizationStatus() -> SystemCalendarAuthorization {
Self.authorizationStatus(
from: EKEventStore.authorizationStatus(for: .event)
)
}
/// Called only from the explicit Settings button. Tool execution uses
/// `readEvents` and therefore can never trigger an iOS permission prompt.
func requestFullAccess() async throws -> SystemCalendarAuthorization {
_ = try await eventStore.requestFullAccessToEvents()
return authorizationStatus()
}
func readEvents(
in interval: DateInterval,
limit: Int
) throws -> SystemCalendarEventPage {
let authorization = authorizationStatus()
guard authorization == .fullAccess else {
throw SystemCalendarReaderError.fullAccessRequired(authorization)
}
let safeLimit = min(max(limit, 1), 11)
let predicate = eventStore.predicateForEvents(
withStart: interval.start,
end: interval.end,
calendars: nil
)
let matchingEvents = eventStore.events(matching: predicate)
.filter { event in
event.startDate < interval.end && event.endDate > interval.start
}
.sorted { first, second in
if first.startDate != second.startDate {
return first.startDate < second.startDate
}
if first.endDate != second.endDate {
return first.endDate < second.endDate
}
return (first.title ?? "").localizedStandardCompare(
second.title ?? ""
) == .orderedAscending
}
let visibleEvents = matchingEvents.prefix(safeLimit).map { event in
SystemCalendarEvent(
title: event.title ?? "Untitled event",
startsAt: event.startDate,
endsAt: max(event.startDate, event.endDate),
isAllDay: event.isAllDay
)
}
return SystemCalendarEventPage(
events: Array(visibleEvents),
hasMore: matchingEvents.count > safeLimit
)
}
private static func authorizationStatus(
from status: EKAuthorizationStatus
) -> SystemCalendarAuthorization {
switch status {
case .fullAccess:
.fullAccess
case .notDetermined:
.notDetermined
case .denied:
.denied
case .restricted:
.restricted
case .writeOnly:
.writeOnly
@unknown default:
.restricted
}
}
}