7e619d0454
Python MCP server (FastMCP) providing Claude Code/CoWork access to Apple Reminders, Calendar, Mail, Contacts, Find My, and Maps via AppleScript. Includes a native SwiftUI config/installer app and a compiled EventKit helper for fast reminder queries on large databases (8,000+ items). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
6.0 KiB
Swift
187 lines
6.0 KiB
Swift
#!/usr/bin/env swift
|
|
// Fast Reminders query via EventKit — handles large reminder databases
|
|
// Usage: swift reminders_helper.swift <command> [args...]
|
|
// lists — list all reminder lists
|
|
// get [--list NAME] [--overdue] [--due-today] [--due-this-week] [--limit N]
|
|
// search <query> [--limit N]
|
|
|
|
import EventKit
|
|
import Foundation
|
|
|
|
let store = EKEventStore()
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
|
|
// Request access
|
|
var accessGranted = false
|
|
if #available(macOS 14.0, *) {
|
|
store.requestFullAccessToReminders { granted, error in
|
|
accessGranted = granted
|
|
semaphore.signal()
|
|
}
|
|
} else {
|
|
store.requestAccess(to: .reminder) { granted, error in
|
|
accessGranted = granted
|
|
semaphore.signal()
|
|
}
|
|
}
|
|
semaphore.wait()
|
|
|
|
guard accessGranted else {
|
|
let err: [String: Any] = ["error": "Reminders access denied. Grant permission in System Settings > Privacy & Security > Reminders."]
|
|
print(String(data: try! JSONSerialization.data(withJSONObject: err), encoding: .utf8)!)
|
|
exit(1)
|
|
}
|
|
|
|
let args = CommandLine.arguments
|
|
let command = args.count > 1 ? args[1] : "lists"
|
|
|
|
func jsonDate(_ date: Date?) -> Any {
|
|
guard let d = date else { return NSNull() }
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime]
|
|
return f.string(from: d)
|
|
}
|
|
|
|
func printJSON(_ obj: Any) {
|
|
let data = try! JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted, .sortedKeys])
|
|
print(String(data: data, encoding: .utf8)!)
|
|
}
|
|
|
|
func getCalendars() -> [EKCalendar] {
|
|
return store.calendars(for: .reminder)
|
|
}
|
|
|
|
func argValue(_ flag: String) -> String? {
|
|
if let idx = args.firstIndex(of: flag), idx + 1 < args.count {
|
|
return args[idx + 1]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasFlag(_ flag: String) -> Bool {
|
|
return args.contains(flag)
|
|
}
|
|
|
|
switch command {
|
|
case "lists":
|
|
let cals = getCalendars()
|
|
let result = cals.map { cal -> [String: Any] in
|
|
// Count incomplete reminders using a predicate
|
|
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: [cal])
|
|
var count = 0
|
|
let countSem = DispatchSemaphore(value: 0)
|
|
store.fetchReminders(matching: pred) { reminders in
|
|
count = reminders?.count ?? 0
|
|
countSem.signal()
|
|
}
|
|
countSem.wait()
|
|
return ["name": cal.title, "id": cal.calendarIdentifier, "incomplete_count": count]
|
|
}
|
|
printJSON(result)
|
|
|
|
case "get":
|
|
let listName = argValue("--list")
|
|
let isOverdue = hasFlag("--overdue")
|
|
let isDueToday = hasFlag("--due-today")
|
|
let isDueThisWeek = hasFlag("--due-this-week")
|
|
let limit = Int(argValue("--limit") ?? "50") ?? 50
|
|
|
|
var calendars: [EKCalendar]? = nil
|
|
if let name = listName {
|
|
calendars = getCalendars().filter { $0.title.lowercased() == name.lowercased() }
|
|
if calendars?.isEmpty == true { calendars = nil }
|
|
}
|
|
|
|
let now = Date()
|
|
let calendar = Calendar.current
|
|
var startDate: Date? = nil
|
|
var endDate: Date? = nil
|
|
|
|
if isOverdue {
|
|
// Reminders due before now
|
|
endDate = now
|
|
startDate = calendar.date(byAdding: .year, value: -10, to: now)
|
|
} else if isDueToday {
|
|
startDate = calendar.startOfDay(for: now)
|
|
endDate = calendar.date(byAdding: .day, value: 1, to: startDate!)
|
|
} else if isDueThisWeek {
|
|
startDate = calendar.startOfDay(for: now)
|
|
endDate = calendar.date(byAdding: .day, value: 7, to: startDate!)
|
|
}
|
|
|
|
let pred = store.predicateForIncompleteReminders(
|
|
withDueDateStarting: startDate,
|
|
ending: endDate,
|
|
calendars: calendars
|
|
)
|
|
|
|
let fetchSem = DispatchSemaphore(value: 0)
|
|
var fetchedReminders: [EKReminder] = []
|
|
store.fetchReminders(matching: pred) { reminders in
|
|
fetchedReminders = reminders ?? []
|
|
fetchSem.signal()
|
|
}
|
|
fetchSem.wait()
|
|
|
|
// Sort by due date (overdue first, then soonest)
|
|
fetchedReminders.sort { a, b in
|
|
let da = a.dueDateComponents?.date ?? Date.distantFuture
|
|
let db = b.dueDateComponents?.date ?? Date.distantFuture
|
|
return da < db
|
|
}
|
|
|
|
let limited = Array(fetchedReminders.prefix(limit))
|
|
let result = limited.map { r -> [String: Any] in
|
|
let dueComps = r.dueDateComponents
|
|
let dueDate: Date? = dueComps?.date
|
|
return [
|
|
"name": r.title ?? "Untitled",
|
|
"list": r.calendar.title,
|
|
"due_date": jsonDate(dueDate),
|
|
"priority": r.priority,
|
|
"completed": r.isCompleted,
|
|
"has_notes": (r.notes != nil && !r.notes!.isEmpty),
|
|
"id": r.calendarItemIdentifier
|
|
]
|
|
}
|
|
printJSON(["count": fetchedReminders.count, "showing": limited.count, "reminders": result])
|
|
|
|
case "search":
|
|
let query = args.count > 2 ? args[2].lowercased() : ""
|
|
let limit = Int(argValue("--limit") ?? "50") ?? 50
|
|
|
|
guard !query.isEmpty else {
|
|
printJSON(["error": "Search query required"])
|
|
exit(1)
|
|
}
|
|
|
|
let pred = store.predicateForIncompleteReminders(withDueDateStarting: nil, ending: nil, calendars: nil)
|
|
let fetchSem = DispatchSemaphore(value: 0)
|
|
var fetchedReminders: [EKReminder] = []
|
|
store.fetchReminders(matching: pred) { reminders in
|
|
fetchedReminders = (reminders ?? []).filter {
|
|
($0.title ?? "").lowercased().contains(query)
|
|
}
|
|
fetchSem.signal()
|
|
}
|
|
fetchSem.wait()
|
|
|
|
let limited = Array(fetchedReminders.prefix(limit))
|
|
let result = limited.map { r -> [String: Any] in
|
|
let dueComps = r.dueDateComponents
|
|
let dueDate: Date? = dueComps?.date
|
|
return [
|
|
"name": r.title ?? "Untitled",
|
|
"list": r.calendar.title,
|
|
"due_date": jsonDate(dueDate),
|
|
"completed": r.isCompleted,
|
|
"id": r.calendarItemIdentifier
|
|
]
|
|
}
|
|
printJSON(["count": fetchedReminders.count, "showing": limited.count, "reminders": result])
|
|
|
|
default:
|
|
printJSON(["error": "Unknown command: \(command). Use: lists, get, search"])
|
|
exit(1)
|
|
}
|