import Foundation // MARK: - JSONValue safe representation enum JSONValue: Codable, Equatable, Sendable { case object([String: JSONValue]) case array([JSONValue]) case string(String) case bool(Bool) case number(Double) case null init() { self = .object([:]) } init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { self = .null return } if let b = try? container.decode(Bool.self) { self = .bool(b) return } if let obj = try? container.decode([String: JSONValue].self) { self = .object(obj) return } if let arr = try? container.decode([JSONValue].self) { self = .array(arr) return } if let s = try? container.decode(String.self) { self = .string(s) return } if let intVal = try? container.decode(Int.self) { self = .number(Double(intVal)) return } if let doubleVal = try? container.decode(Double.self) { self = .number(doubleVal) return } throw DecodingError.typeMismatch(JSONValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unrecognized JSON value")) } func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { case .null: try container.encodeNil() case .bool(let b): try container.encode(b) case .string(let s): try container.encode(s) case .number(let d): if d.truncatingRemainder(dividingBy: 1) == 0, d >= Double(Int.min), d <= Double(Int.max) { try container.encode(Int(d)) } else { try container.encode(d) } case .object(let o): try container.encode(o) case .array(let a): try container.encode(a) } } } typealias Args = JSONValue struct CalendarListItem: Codable, Equatable, Sendable { let id: String let title: String let source: String let type: String } struct CalendarEventItem: Codable, Equatable, Sendable { let id: String let title: String let start: String let end: String let all_day: Bool let calendar_id: String let calendar_title: String let notes: String? let location: String? enum CodingKeys: String, CodingKey { case id case title case start case end case all_day case calendar_id case calendar_title case notes case location } } enum CalendarProviderError: Error, Equatable, Sendable { case permissionRequired case unavailable(String) case invalidRequest(String) var isPermission: Bool { if case .permissionRequired = self { return true } return false } } protocol CalendarListProviding: Sendable { func listCalendars() throws -> [CalendarListItem] } protocol CalendarEventsListProviding: Sendable { func listEvents(start: Date, end: Date, calendarId: String?, calendarTitle: String?, limit: Int) throws -> [CalendarEventItem] } protocol CalendarEventCreateProviding: Sendable { func createEvent( title: String, start: Date, end: Date, allDay: Bool, notes: String?, location: String?, calendarId: String?, calendarTitle: String? ) throws -> CalendarEventItem } protocol FullCalendarProviding: CalendarListProviding, CalendarEventsListProviding, CalendarEventCreateProviding {} struct DefaultCalendarListProvider: CalendarListProviding { func listCalendars() throws -> [CalendarListItem] { throw CalendarProviderError.unavailable("no provider configured") } } struct Request: Codable, Sendable { let id: String let operation: String let arguments: JSONValue } struct ResultPayload: Codable, Equatable, Sendable { let protocol_version: String let operation: String let status: String? let calendars: [CalendarListItem]? let events: [CalendarEventItem]? let event: CalendarEventItem? let contacts: [ContactListItem]? let contact: ContactDetailItem? let created_contact: ContactCreateResult? let reminder_lists: [ReminderListItem]? let reminders: [ReminderItem]? let reminder: ReminderItem? let created_reminder: ReminderCreateResult? let system_info: SystemInfoItem? let speech_api_status: SpeechApiStatusItem? init( protocol_version: String, operation: String, status: String? = nil, calendars: [CalendarListItem]? = nil, events: [CalendarEventItem]? = nil, event: CalendarEventItem? = nil, contacts: [ContactListItem]? = nil, contact: ContactDetailItem? = nil, created_contact: ContactCreateResult? = nil, reminder_lists: [ReminderListItem]? = nil, reminders: [ReminderItem]? = nil, reminder: ReminderItem? = nil, created_reminder: ReminderCreateResult? = nil, system_info: SystemInfoItem? = nil, speech_api_status: SpeechApiStatusItem? = nil ) { self.protocol_version = protocol_version self.operation = operation self.status = status self.calendars = calendars self.events = events self.event = event self.contacts = contacts self.contact = contact self.created_contact = created_contact self.reminder_lists = reminder_lists self.reminders = reminders self.reminder = reminder self.created_reminder = created_reminder self.system_info = system_info self.speech_api_status = speech_api_status } } struct ErrorPayload: Codable, Equatable, Sendable { let code: String let message: String } struct Response: Codable, Equatable, Sendable { let id: String let ok: Bool let result: ResultPayload? let error: ErrorPayload? } let PROTOCOL_VERSION = "1.0.0" // MARK: - Shared ISO8601 parsing & validation helpers enum CalendarArgsValidationError: Error, Equatable, Sendable { case invalid(String) } nonisolated(unsafe) private let iso8601WithFractional: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return f }() nonisolated(unsafe) private let iso8601Plain: ISO8601DateFormatter = { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime] return f }() func parseISO8601DateStrict(_ s: String) throws -> Date { if let d = iso8601WithFractional.date(from: s) { return d } if let d = iso8601Plain.date(from: s) { return d } throw CalendarProviderError.invalidRequest("Invalid ISO-8601 date: \(s)") } func extractStringArg(_ args: JSONValue, key: String) -> String? { guard case .object(let obj) = args, let val = obj[key] else { return nil } switch val { case .string(let s): return s case .null: return nil default: return nil } } func extractBoolArg(_ args: JSONValue, key: String, defaultValue: Bool) -> Bool { guard case .object(let obj) = args, let val = obj[key] else { return defaultValue } switch val { case .bool(let b): return b case .object(_), .array(_), .string(_), .number(_), .null: return defaultValue } } func extractIntArg(_ args: JSONValue, key: String) -> Int? { guard case .object(let obj) = args, let val = obj[key] else { return nil } switch val { case .number(let n): return Int(n) case .string(let s): return Int(s) default: return nil } } func iso8601StringFromDate(_ d: Date) -> String { let f = ISO8601DateFormatter() f.formatOptions = [.withInternetDateTime] return f.string(from: d) } func dispatch( request: Request, calendarProvider: CalendarListProviding = ProductionCalendarProvider(), authProvider: CalendarAuthorizationProviding = ProductionAuthProvider() ) -> Response { return dispatch( request: request, calendarProvider: calendarProvider, eventsProvider: ProductionCalendarEventsProvider(), createProvider: ProductionCalendarEventCreateProvider(), authProvider: authProvider, contactsAuthProvider: ProductionContactsAuthProvider(), contactsSearchProvider: ProductionContactsSearchProvider(), contactsReadProvider: ProductionContactsReadProvider(), contactsCreateProvider: ProductionContactsCreateProvider(), remindersAuthProvider: ProductionRemindersAuthProvider(), remindersListsProvider: ProductionRemindersListsProvider(), remindersListProvider: ProductionRemindersListProvider(), remindersCreateProvider: ProductionRemindersCreateProvider() ) } func dispatch( request: Request, calendarProvider: CalendarListProviding, eventsProvider: CalendarEventsListProviding, createProvider: CalendarEventCreateProviding, authProvider: CalendarAuthorizationProviding, contactsAuthProvider: ContactsAuthorizationProviding = ProductionContactsAuthProvider(), contactsSearchProvider: ContactsSearchProviding = ProductionContactsSearchProvider(), contactsReadProvider: ContactsReadProviding = ProductionContactsReadProvider(), contactsCreateProvider: ContactsCreateProviding = ProductionContactsCreateProvider(), remindersAuthProvider: RemindersAuthorizationProviding = ProductionRemindersAuthProvider(), remindersListsProvider: RemindersListsProviding = ProductionRemindersListsProvider(), remindersListProvider: RemindersListProviding = ProductionRemindersListProvider(), remindersCreateProvider: RemindersCreateProviding = ProductionRemindersCreateProvider() ) -> Response { switch request.operation { case "service.health": let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "service.health", status: "ok", calendars: nil ) return Response(id: request.id, ok: true, result: result, error: nil) case "calendar.list": do { let items = try calendarProvider.listCalendars() let sorted = items.sorted { if $0.source != $1.source { return $0.source < $1.source } if $0.title != $1.title { return $0.title < $1.title } return $0.id < $1.id } let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "calendar.list", status: "ok", calendars: sorted ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as CalendarProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "calendar.request_full_access": do { let status = authProvider.authorizationStatus() if status == .authorized { let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "calendar.request_full_access", status: "authorized", calendars: nil ) return Response(id: request.id, ok: true, result: result, error: nil) } let granted: Bool do { granted = try authProvider.requestFullAccess() } catch let err as CalendarProviderError { switch err { case .unavailable(let msg): let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .permissionRequired: let e = ErrorPayload(code: "permission_denied", message: "Calendar access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } if granted { let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "calendar.request_full_access", status: "authorized", calendars: nil ) return Response(id: request.id, ok: true, result: result, error: nil) } else { let e = ErrorPayload(code: "permission_denied", message: "Calendar access denied. Grant permission in System Settings > Privacy & Security > Calendars.") return Response(id: request.id, ok: false, result: nil, error: e) } } case "calendar.events.list": do { guard let startStr = extractStringArg(request.arguments, key: "start"), !startStr.isEmpty else { let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: start") return Response(id: request.id, ok: false, result: nil, error: e) } guard let endStr = extractStringArg(request.arguments, key: "end"), !endStr.isEmpty else { let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: end") return Response(id: request.id, ok: false, result: nil, error: e) } let startDate: Date let endDate: Date do { startDate = try parseISO8601DateStrict(startStr) } catch let err as CalendarProviderError { if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) } throw err } do { endDate = try parseISO8601DateStrict(endStr) } catch let err as CalendarProviderError { if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) } throw err } if startDate >= endDate { let e = ErrorPayload(code: "invalid_request", message: "start must occur before end") return Response(id: request.id, ok: false, result: nil, error: e) } var limit = 50 if let extracted = extractIntArg(request.arguments, key: "limit") { limit = extracted } if limit < 1 || limit > 250 { let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 250") return Response(id: request.id, ok: false, result: nil, error: e) } let calId = extractStringArg(request.arguments, key: "calendar_id") ?? extractStringArg(request.arguments, key: "calendarId") let calTitle = extractStringArg(request.arguments, key: "calendar") ?? extractStringArg(request.arguments, key: "calendar_title") ?? extractStringArg(request.arguments, key: "calendarTitle") let items = try eventsProvider.listEvents(start: startDate, end: endDate, calendarId: calId, calendarTitle: calTitle, limit: limit) let sorted = items.sorted { if $0.start != $1.start { return $0.start < $1.start } if $0.title != $1.title { return $0.title < $1.title } return $0.id < $1.id } let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "calendar.events.list", status: "ok", events: sorted ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as CalendarProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "calendar.event.create": do { guard let title = extractStringArg(request.arguments, key: "title"), !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: title") return Response(id: request.id, ok: false, result: nil, error: e) } guard let startStr = extractStringArg(request.arguments, key: "start"), !startStr.isEmpty else { let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: start") return Response(id: request.id, ok: false, result: nil, error: e) } guard let endStr = extractStringArg(request.arguments, key: "end"), !endStr.isEmpty else { let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: end") return Response(id: request.id, ok: false, result: nil, error: e) } let startDate: Date let endDate: Date do { startDate = try parseISO8601DateStrict(startStr) } catch let err as CalendarProviderError { if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) } throw err } do { endDate = try parseISO8601DateStrict(endStr) } catch let err as CalendarProviderError { if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) } throw err } if startDate >= endDate { let e = ErrorPayload(code: "invalid_request", message: "start must occur before end") return Response(id: request.id, ok: false, result: nil, error: e) } let allDay = extractBoolArg(request.arguments, key: "all_day", defaultValue: extractBoolArg(request.arguments, key: "allDay", defaultValue: false)) let notes = extractStringArg(request.arguments, key: "notes") let location = extractStringArg(request.arguments, key: "location") let calId = extractStringArg(request.arguments, key: "calendar_id") ?? extractStringArg(request.arguments, key: "calendarId") let calTitle = extractStringArg(request.arguments, key: "calendar") ?? extractStringArg(request.arguments, key: "calendar_title") ?? extractStringArg(request.arguments, key: "calendarTitle") let created = try createProvider.createEvent( title: title, start: startDate, end: endDate, allDay: allDay, notes: notes, location: location, calendarId: calId, calendarTitle: calTitle ) let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "calendar.event.create", status: "ok", event: created ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as CalendarProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Calendar access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "calendar_unavailable", message: msg.isEmpty ? "Calendar unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "calendar_unavailable", message: "Calendar unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "contacts.request_access": do { let status = contactsAuthProvider.authorizationStatus() if status == .authorized { let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "contacts.request_access", status: "authorized" ) return Response(id: request.id, ok: true, result: result, error: nil) } let granted: Bool do { granted = try contactsAuthProvider.requestAccess() } catch let err as ContactsProviderError { switch err { case .unavailable(let msg): let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .permissionRequired, .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) case .notFound(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } if granted { let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "contacts.request_access", status: "authorized" ) return Response(id: request.id, ok: true, result: result, error: nil) } else { let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied. Grant permission in System Settings > Privacy & Security > Contacts.") return Response(id: request.id, ok: false, result: nil, error: e) } } case "contacts.search": do { var limit = 50 if let extracted = extractIntArg(request.arguments, key: "limit") { limit = extracted } if limit < 1 || limit > 100 { let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 100") return Response(id: request.id, ok: false, result: nil, error: e) } let queryObj: String? if case .object(let obj) = request.arguments { if let q = obj["query"] { switch q { case .string(let s): queryObj = s case .null: queryObj = nil default: queryObj = nil } } else { queryObj = nil } } else { queryObj = nil } // Also accept empty string? legacy allowed query nil let items = try contactsSearchProvider.searchContacts(query: queryObj, limit: limit) let sorted = items.sorted { if $0.name != $1.name { return $0.name < $1.name } if $0.organization != $1.organization { return $0.organization < $1.organization } return $0.id < $1.id } let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "contacts.search", status: "ok", contacts: sorted ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as ContactsProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) case .notFound(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "contacts.read": do { guard let idVal = extractStringArg(request.arguments, key: "id"), !idVal.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: id") return Response(id: request.id, ok: false, result: nil, error: e) } let item = try contactsReadProvider.readContact(id: idVal) let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "contacts.read", status: "ok", contact: item ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as ContactsProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) case .notFound(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "contacts.create": do { // Legacy keys: firstName/lastName/organization/jobTitle/note + email/phone objects var firstName: String? = extractStringArg(request.arguments, key: "firstName") ?? extractStringArg(request.arguments, key: "first_name") var lastName: String? = extractStringArg(request.arguments, key: "lastName") ?? extractStringArg(request.arguments, key: "last_name") var organization: String? = extractStringArg(request.arguments, key: "organization") var jobTitle: String? = extractStringArg(request.arguments, key: "jobTitle") ?? extractStringArg(request.arguments, key: "job_title") var note: String? = extractStringArg(request.arguments, key: "note") // At least one of firstName/lastName/organization required to create meaningful contact let hasName = !(firstName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) || !(lastName?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) || !(organization?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) if !hasName { // allow empty still? per legacy it allowed empty, but we require at least something // However preserve compat: if nothing, invalid_request let e = ErrorPayload(code: "invalid_request", message: "Missing required field: at least firstName, lastName, or organization required") return Response(id: request.id, ok: false, result: nil, error: e) } var emailLV: ContactEmailLabelValue? = nil var phoneLV: ContactPhoneLabelValue? = nil if case .object(let outer) = request.arguments { if let ev = outer["email"] { if case .object(let eObj) = ev { let label: String let value: String switch eObj["label"] { case .string(let s): label = s default: label = "work" } switch eObj["value"] { case .string(let s): value = s default: value = "" } if !value.isEmpty { emailLV = ContactEmailLabelValue(label: label, value: value) } } } if let pv = outer["phone"] { if case .object(let pObj) = pv { let label: String let value: String switch pObj["label"] { case .string(let s): label = s default: label = "mobile" } switch pObj["value"] { case .string(let s): value = s default: value = "" } if !value.isEmpty { phoneLV = ContactPhoneLabelValue(label: label, value: value) } } } } let created = try contactsCreateProvider.createContact( firstName: firstName, lastName: lastName, organization: organization, jobTitle: jobTitle, note: note, email: emailLV, phone: phoneLV ) let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "contacts.create", status: "ok", created_contact: created ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as ContactsProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Contacts access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Contacts access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "contacts_unavailable", message: msg.isEmpty ? "Contacts unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) case .notFound(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "contacts_unavailable", message: "Contacts unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "reminders.request_full_access": do { let status = remindersAuthProvider.authorizationStatus() if status == .authorized { let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "reminders.request_full_access", status: "authorized" ) return Response(id: request.id, ok: true, result: result, error: nil) } let granted: Bool do { granted = try remindersAuthProvider.requestFullAccess() } catch let err as RemindersProviderError { switch err { case .unavailable(let msg): let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .permissionRequired, .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } if granted { let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "reminders.request_full_access", status: "authorized" ) return Response(id: request.id, ok: true, result: result, error: nil) } else { let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied. Grant permission in System Settings > Privacy & Security > Reminders.") return Response(id: request.id, ok: false, result: nil, error: e) } } case "reminders.lists": do { let items = try remindersListsProvider.listReminderLists() let sorted = items.sorted { if $0.source != $1.source { return $0.source < $1.source } if $0.title != $1.title { return $0.title < $1.title } return $0.id < $1.id } let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "reminders.lists", status: "ok", reminder_lists: sorted ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as RemindersProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "reminders.list": do { var limit = 50 if let extracted = extractIntArg(request.arguments, key: "limit") { limit = extracted } if limit < 1 || limit > 250 { let e = ErrorPayload(code: "invalid_request", message: "limit must be between 1 and 250") return Response(id: request.id, ok: false, result: nil, error: e) } let listId = extractStringArg(request.arguments, key: "list_id") ?? extractStringArg(request.arguments, key: "listId") ?? extractStringArg(request.arguments, key: "id") let listTitle = extractStringArg(request.arguments, key: "list") ?? extractStringArg(request.arguments, key: "list_title") ?? extractStringArg(request.arguments, key: "listTitle") var completedFilter: Bool? = nil if case .object(let obj) = request.arguments { if let compVal = obj["completed"] { switch compVal { case .bool(let b): completedFilter = b case .null: completedFilter = nil default: break } } } let items = try remindersListProvider.listReminders(listId: listId, listTitle: listTitle, completed: completedFilter, limit: limit) let sorted = items.sorted { let due0 = $0.due ?? "" let due1 = $1.due ?? "" if due0 != due1 { return due0 < due1 } if $0.title != $1.title { return $0.title < $1.title } return $0.id < $1.id } let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "reminders.list", status: "ok", reminders: sorted ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as RemindersProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "reminders.create": do { guard let title = extractStringArg(request.arguments, key: "title"), !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { let e = ErrorPayload(code: "invalid_request", message: "Missing required argument: title") return Response(id: request.id, ok: false, result: nil, error: e) } if title.count > 1024 { let e = ErrorPayload(code: "invalid_request", message: "title exceeds maximum length 1024") return Response(id: request.id, ok: false, result: nil, error: e) } let notes = extractStringArg(request.arguments, key: "notes") ?? extractStringArg(request.arguments, key: "body") if let n = notes, n.count > 4096 { let e = ErrorPayload(code: "invalid_request", message: "notes exceeds maximum length 4096") return Response(id: request.id, ok: false, result: nil, error: e) } var dueDate: Date? = nil if let dueStr = extractStringArg(request.arguments, key: "due") ?? extractStringArg(request.arguments, key: "dueDate") ?? extractStringArg(request.arguments, key: "due_date") { if !dueStr.isEmpty { do { dueDate = try parseISO8601DateStrict(dueStr) } catch let err as CalendarProviderError { if case .invalidRequest(let msg) = err { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "invalid_request", message: msg)) } throw err } } } var priority: Int? = nil if let p = extractIntArg(request.arguments, key: "priority") { if p < 0 || p > 9 { let e = ErrorPayload(code: "invalid_request", message: "priority must be between 0 and 9") return Response(id: request.id, ok: false, result: nil, error: e) } priority = p } let listId = extractStringArg(request.arguments, key: "list_id") ?? extractStringArg(request.arguments, key: "listId") let listTitle = extractStringArg(request.arguments, key: "list") ?? extractStringArg(request.arguments, key: "list_title") ?? extractStringArg(request.arguments, key: "listTitle") if (listId == nil || listId?.isEmpty == true) && (listTitle == nil || listTitle?.isEmpty == true) { let e = ErrorPayload(code: "invalid_request", message: "Reminders list must be specified by id or exact unique title") return Response(id: request.id, ok: false, result: nil, error: e) } let created = try remindersCreateProvider.createReminder(title: title, listId: listId, listTitle: listTitle, notes: notes, due: dueDate, priority: priority) let result = ResultPayload( protocol_version: PROTOCOL_VERSION, operation: "reminders.create", status: "ok", created_reminder: created ) return Response(id: request.id, ok: true, result: result, error: nil) } catch let err as RemindersProviderError { switch err { case .permissionRequired: let e = ErrorPayload(code: "permission_required", message: "Reminders access not authorized. Grant permission in System Settings.") return Response(id: request.id, ok: false, result: nil, error: e) case .permissionDenied: let e = ErrorPayload(code: "permission_denied", message: "Reminders access denied.") return Response(id: request.id, ok: false, result: nil, error: e) case .unavailable(let msg): let e = ErrorPayload(code: "reminders_unavailable", message: msg.isEmpty ? "Reminders unavailable" : msg) return Response(id: request.id, ok: false, result: nil, error: e) case .invalidRequest(let msg): let e = ErrorPayload(code: "invalid_request", message: msg) return Response(id: request.id, ok: false, result: nil, error: e) } } catch { let e = ErrorPayload(code: "reminders_unavailable", message: "Reminders unavailable") return Response(id: request.id, ok: false, result: nil, error: e) } case "system.get_info": do { let info = try ProductionSystemInfoProvider().getSystemInfo() let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "system.get_info", status: "ok", system_info: info) return Response(id: request.id, ok: true, result: result, error: nil) } catch { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "system_unavailable", message: "System info unavailable")) } case "system.speech_api_status": do { let st = try ProductionSystemInfoProvider().getSpeechApiStatus() let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "system.speech_api_status", status: "ok", speech_api_status: st) return Response(id: request.id, ok: true, result: result, error: nil) } catch { return Response(id: request.id, ok: false, result: nil, error: ErrorPayload(code: "system_unavailable", message: "Speech API status unavailable")) } case "apple_llm.check": // Minimal health probe — returns status ok with notes that full ANE session is owned separately let result = ResultPayload(protocol_version: PROTOCOL_VERSION, operation: "apple_llm.check", status: "ok") return Response(id: request.id, ok: true, result: result, error: nil) default: let err = ErrorPayload( code: "unknown_operation", message: "Unknown operation: \(request.operation)" ) return Response(id: request.id, ok: false, result: nil, error: err) } } func dispatch(request: Request) -> Response { return dispatch(request: request, calendarProvider: ProductionCalendarProvider(), authProvider: ProductionAuthProvider()) } func ProductionCalendarProvider() -> CalendarListProviding { return EventKitCalendarProvider() } func ProductionAuthProvider() -> CalendarAuthorizationProviding { return EventKitCalendarAuthorizationProvider() } func ProductionCalendarEventsProvider() -> CalendarEventsListProviding { return EventKitCalendarProvider() } func ProductionCalendarEventCreateProvider() -> CalendarEventCreateProviding { return EventKitCalendarProvider() } func ProductionContactsAuthProvider() -> ContactsAuthorizationProviding { return ContactsAuthorizationProvider() } func ProductionContactsSearchProvider() -> ContactsSearchProviding { return ContactsSearchProvider() } func ProductionContactsReadProvider() -> ContactsReadProviding { return ContactsReadProvider() } func ProductionContactsCreateProvider() -> ContactsCreateProviding { return ContactsCreateProvider() } func ProductionRemindersAuthProvider() -> RemindersAuthorizationProviding { return RemindersAuthorizationProvider() } func ProductionRemindersListsProvider() -> RemindersListsProviding { return EventKitRemindersProvider() } func ProductionRemindersListProvider() -> RemindersListProviding { return EventKitRemindersProvider() } func ProductionRemindersCreateProvider() -> RemindersCreateProviding { return EventKitRemindersProvider() }