Skip to content

Instantly share code, notes, and snippets.

@htlin222
Created April 22, 2026 07:43
Show Gist options
  • Select an option

  • Save htlin222/733bcabaa447760693c82108a0d58065 to your computer and use it in GitHub Desktop.

Select an option

Save htlin222/733bcabaa447760693c82108a0d58065 to your computer and use it in GitHub Desktop.
Calendar Ops Notes — querying Apple Calendar.sqlitedb, Google Calendar MCP, AppleScript write-back (macOS)

Calendar Ops Notes (macOS + Google + Apple)

在 Claude Code / 一般 shell 裡查詢、管理行事曆的操作手冊。 下次不用從頭摸。


TL;DR — 帳號 / 工具對照

帳號類型 讀取 寫入(建議) 寫入(不建議)
Google Calendar Google Calendar MCP Google Calendar MCP
iCloud Calendar.sqlitedb (read-only) AppleScript / EventKit 直接寫 sqlite
Exchange / CalDAV(公司、院內) Calendar.sqlitedb (read-only) AppleScript 直接寫 sqlite
URL 訂閱 (.ics) Calendar.sqlitedb 改不了(唯讀來源)

規則:讀 sqlite OK,寫絕對走正規 API。


本機 Calendar Database

位置

~/Library/Group Containers/group.com.apple.calendar/Calendar.sqlitedb

舊的 ~/Library/Calendars/ 在新版 macOS 幾乎空著,真正資料在上面那個。

關鍵 tables

Table 作用
Calendar 所有 calendar(含訂閱、group、iCloud、Exchange 等)
Store 帳號 / 資料來源(iCloud、各 Gmail、Exchange、Subscribed Calendars…)
CalendarItem 事件本體
Location 地點
OccurrenceCache 重複事件展開後的每個 instance
Alarm 提醒

Apple epoch 換算

Apple 用 NSDate:2001-01-01 00:00:00 UTC 開始的秒數。

apple_ts = unix_ts - 978307200

SQL 裡要顯示可讀時間:

datetime(col + 978307200, 'unixepoch', 'localtime')

產生查詢窗的 Python one-liner:

python3 -c "from datetime import datetime, timezone, timedelta; \
  tz=timezone(timedelta(hours=8)); \
  print(int(datetime(2026,4,25,0,0,tzinfo=tz).timestamp() - 978307200))"

常用 Query

1. 列出所有 calendar(含帳號來源)

SELECT c.title, c.type, s.name AS account, c.subcal_url
FROM Calendar c
LEFT JOIN Store s ON c.store_id = s.ROWID
ORDER BY s.name, c.title;
  • subcal_url 有值 = URL 訂閱的 calendar(例如 webcal://… / .ics
  • s.name = 'Subscribed Calendars' = 純 URL 訂閱帳號

2. 找出所有 URL 訂閱

SELECT title, subcal_url, refresh_interval
FROM Calendar
WHERE subcal_url IS NOT NULL AND subcal_url != '';

3. 某段時間、某些 calendar 的事件

SELECT c.title AS cal,
       datetime(ci.start_date + 978307200, 'unixepoch', 'localtime') AS start,
       datetime(ci.end_date   + 978307200, 'unixepoch', 'localtime') AS end,
       ci.all_day,
       ci.summary,
       l.title AS location
FROM CalendarItem ci
JOIN Calendar c     ON ci.calendar_id = c.ROWID
LEFT JOIN Location l ON ci.location_id = l.ROWID
WHERE c.title IN ('<calendar-A>', '<calendar-B>')
  AND ci.hidden = 0
  AND ci.status != 2             -- 排除 cancelled
  AND ci.end_date   >= :window_start
  AND ci.start_date <= :window_end
ORDER BY ci.start_date;

4. 重複事件 (recurring) 也一起抓

重複事件的單次 instance 存在 OccurrenceCache

SELECT c.title AS cal,
       datetime(oc.occurrence_start_date + 978307200, 'unixepoch', 'localtime') AS start,
       datetime(oc.occurrence_end_date   + 978307200, 'unixepoch', 'localtime') AS end,
       ci.summary,
       l.title AS loc
FROM OccurrenceCache oc
JOIN CalendarItem ci ON oc.event_id     = ci.ROWID
JOIN Calendar     c  ON oc.calendar_id  = c.ROWID
LEFT JOIN Location l ON ci.location_id  = l.ROWID
WHERE oc.occurrence_end_date   > :window_start
  AND oc.occurrence_start_date < :window_end
  AND ci.hidden = 0
ORDER BY oc.occurrence_start_date;

⚠️ 記得 SELECT DISTINCT 或用 event_id 分組,不然 occurrence 會重複出現多次(每個 day row 一份)。

5. 關鍵字搜尋(某類會議整年度)

SELECT datetime(ci.start_date + 978307200, 'unixepoch', 'localtime') AS start,
       ci.summary,
       ci.description
FROM CalendarItem ci
JOIN Calendar c ON ci.calendar_id = c.ROWID
WHERE c.title = '<calendar-name>'
  AND ci.hidden = 0
  AND ci.status != 2
  AND ci.start_date BETWEEN :year_start AND :year_end
  AND ci.summary LIKE '%<keyword>%'
ORDER BY ci.start_date;

為什麼不要直接寫 sqlite

  1. Calendar.app / CalendarAgent 常駐打開檔案 — 寫入可能被覆蓋或造成 WAL 損毀
  2. 繞過同步層 — CalendarAgent 把變更推到 iCloud / Google / Exchange,直接改 sqlite 等於只改本機
  3. 內部快取要同步維護OccurrenceCache, OccurrenceCacheDays, Notification, Alarm, CalendarChanges 互相綁 trigger
  4. TCC 權限 — 新版 macOS 對 Group Containers 寫入會擋

正規寫入方法

Google Calendar

Claude 的 google_calendar MCP:create_event / update_event / delete_event / list_events / suggest_time

iCloud / 本機 calendar — AppleScript

tell application "Calendar"
  tell calendar "<calendar-name>"
    make new event with properties {
      summary:"<title>",
      start date:(current date),
      end date:(current date) + 1 * hours,
      location:"<loc>"
    }
  end tell
end tell

CLI:osascript -e '...'osascript /path/to/script.applescript

EventKit (Swift / ObjC)

需要 TCC 授權,適合寫獨立 app / 背景 daemon。一次性腳本不划算,用 AppleScript 就好。


好用的 debug 小技巧

找出 TODAY / NEXT WEEK 的 Apple timestamp

python3 -c "
from datetime import datetime, timezone, timedelta
tz = timezone(timedelta(hours=8))
def apple(d): return int(d.timestamp() - 978307200)
today = datetime.now(tz).replace(hour=0,minute=0,second=0,microsecond=0)
print('today     :', apple(today))
print('today +7d :', apple(today + timedelta(days=7)))
"

看某個事件的原始 description(帶附件 URL / Zoom / Jitsi)

SELECT ci.summary, ci.description
FROM CalendarItem ci
WHERE ci.summary LIKE '%<keyword>%'
LIMIT 5;

Exchange 推來的 description 常常夾 HTML / Zoom 連結,直接 grep 就能挖到會議連結。


風險 / 注意

  • 這份 DB 受 macOS TCC (Transparency, Consent, and Control) 保護,跑在某些沙盒 shell 裡可能被拒(需要 Full Disk Access 或在 iTerm2/Terminal 主 shell 跑)
  • 不要把查詢結果原樣上傳到公開空間(事件 description 可能含病人資訊、會議連結、帳號密碼、附件 URL)
  • Calendar.app 關掉再查可以避免 -wal / -shm 半寫狀態造成結果不一致(通常不會影響讀)

可延伸的下一步

  • 寫一個 cal-week bash 函式,帶日期參數,回傳指定週的表格
  • 寫一個 cal-subs 列出所有 URL 訂閱與 refresh 狀態
  • 做一個 AppleScript 範本:從 markdown 清單批次加事件到指定 calendar
  • 研究 EventKit CLI 工具(例如 icalBuddy, dateutils)是否能取代手寫 SQL
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment