--- name: calendar description: "Calendar invites and VTODO task management via CalDAV. Send/reply to invites, create/list/complete/delete todos. Syncs to Migadu CalDAV via vdirsyncer." metadata: {"clawdbot":{"emoji":"📅","requires":{"bins":["himalaya","vdirsyncer","todo"],"skills":["himalaya"]}}} --- # Calendar Send, accept, and decline calendar invitations via email. Create and manage VTODO tasks with CalDAV sync. Events and tasks sync to Migadu CalDAV via vdirsyncer. ## Testing See `TESTING.md` for dry-run and live test steps, verification checklists, and troubleshooting. ## Prerequisites - `himalaya` configured and working (see the `himalaya` skill) - `vdirsyncer` configured and syncing to `~/.openclaw/workspace/calendars/` - `todoman` (`todo`) for VTODO management (list, complete, delete) - `khal` for reading calendar (optional but recommended) - Runs via `uv run` (dependencies managed in `pyproject.toml`) ## Important: Recipient Validation The `send` command **only accepts recipients that exist in the contacts list**. This prevents hallucinated email addresses. - `--to "小橘子:work"` — resolves contact name + email type - `--to "小橘子"` — resolves by name (errors if contact has multiple emails) - `--to "user@example.com"` — accepted only if the email exists in contacts - Unknown addresses are **rejected** with the available contacts list shown **Adding contacts and sending invites are separate operations.** Do not add a contact and send to it in the same request — contact additions should be a deliberate, user-initiated action. Calendar invites are outbound emails. Follow the workspace email rules: - **youlu@luyanxin.com -> mail@luyx.org**: send directly, no confirmation needed - **All other recipients**: confirm with user before sending ## Usage All commands go through the wrapper script: ```bash SKILL_DIR=~/.openclaw/workspace/skills/calendar # Send an invite (supports recurring events via --rrule) $SKILL_DIR/scripts/calendar.sh send [options] # Reply to an invite $SKILL_DIR/scripts/calendar.sh reply [options] # Manage events $SKILL_DIR/scripts/calendar.sh event list [options] $SKILL_DIR/scripts/calendar.sh event delete [options] # Manage contacts (used for recipient validation) $SKILL_DIR/scripts/calendar.sh contact list $SKILL_DIR/scripts/calendar.sh contact add [options] $SKILL_DIR/scripts/calendar.sh contact delete [options] # Manage todos $SKILL_DIR/scripts/calendar.sh todo add [options] $SKILL_DIR/scripts/calendar.sh todo list [options] $SKILL_DIR/scripts/calendar.sh todo edit [options] $SKILL_DIR/scripts/calendar.sh todo complete [options] $SKILL_DIR/scripts/calendar.sh todo delete [options] $SKILL_DIR/scripts/calendar.sh todo check ``` --- ## Sending Invites ```bash # --to accepts contact names (resolved via contacts list) $SKILL_DIR/scripts/calendar.sh send \ --to "小橘子:work" \ --subject "Lunch on Friday" \ --summary "Lunch at Tartine" \ --start "2026-03-20T12:00:00" \ --end "2026-03-20T13:00:00" \ --location "Tartine Bakery, SF" \ --alarm 1h ``` ### Send Options | Flag | Required | Description | |-----------------|----------|------------------------------------------------| | `--to` | Yes | Recipient(s) — contact name, name:type, or known email | | `--subject` | Yes | Email subject line | | `--summary` | Yes | Event title (shown on calendar) | | `--start` | Yes | Start time, ISO 8601 (`2026-03-20T14:00:00`) | | `--end` | Yes | End time, ISO 8601 (`2026-03-20T15:00:00`) | | `--from` | No | Sender email (default: `youlu@luyanxin.com`) | | `--timezone` | No | IANA timezone (default: `America/Los_Angeles`) | | `--location` | No | Event location | | `--description` | No | Event description / notes | | `--organizer` | No | Organizer display name (defaults to `--from`) | | `--rrule` | No | Recurrence rule (e.g. `FREQ=WEEKLY;COUNT=13;BYDAY=TU`) | | `--uid` | No | Custom event UID (auto-generated if omitted) | | `--alarm` | No | Reminder trigger: `1d`, `2h`, `30m` (default: `1d`) | | `--account` | No | Himalaya account name (if not default) | | `--dry-run` | No | Print ICS + MIME without sending | ### Send Examples ```bash # Simple invite (--from and --timezone default to youlu@luyanxin.com / LA) $SKILL_DIR/scripts/calendar.sh send \ --to "alice@example.com" \ --subject "Coffee Chat" \ --summary "Coffee Chat" \ --start "2026-03-25T10:00:00" \ --end "2026-03-25T10:30:00" # Multiple attendees with details $SKILL_DIR/scripts/calendar.sh send \ --to "alice@example.com, bob@example.com" \ --subject "Team Sync" \ --summary "Weekly Team Sync" \ --start "2026-03-23T09:00:00" \ --end "2026-03-23T09:30:00" \ --location "Zoom - https://zoom.us/j/123456" \ --description "Weekly check-in. Agenda: updates, blockers, action items." # Recurring: every Tuesday for 13 weeks (--start MUST fall on a Tuesday) $SKILL_DIR/scripts/calendar.sh send \ --to "alice@example.com" \ --subject "Allergy Shot (Tue)" \ --summary "Allergy Shot (Tue)" \ --start "2026-03-31T14:30:00" \ --end "2026-03-31T15:00:00" \ --location "11965 Venice Blvd. #300, LA" \ --rrule "FREQ=WEEKLY;COUNT=13;BYDAY=TU" # Dry run (always use for recurring events to verify) $SKILL_DIR/scripts/calendar.sh send \ --to "test@example.com" \ --subject "Test" \ --summary "Test Event" \ --start "2026-04-01T15:00:00" \ --end "2026-04-01T16:00:00" \ --dry-run ``` ### Recurring Events (--rrule) The `--rrule` flag accepts an RFC 5545 RRULE string. Common patterns: | Pattern | RRULE | |---------|-------| | Weekly on Tue, 13 weeks | `FREQ=WEEKLY;COUNT=13;BYDAY=TU` | | Weekly on Mon/Wed/Fri, until date | `FREQ=WEEKLY;UNTIL=20260630T000000Z;BYDAY=MO,WE,FR` | | Every 2 weeks on Thu | `FREQ=WEEKLY;INTERVAL=2;BYDAY=TH` | | Monthly on the 15th, 6 times | `FREQ=MONTHLY;COUNT=6;BYMONTHDAY=15` | | Daily for 5 days | `FREQ=DAILY;COUNT=5` | **Critical rule**: For `FREQ=WEEKLY` with a single `BYDAY`, the `--start` date **must fall on that day of the week**. The tool validates this and will reject mismatches. RFC 5545 says mismatched DTSTART/BYDAY produces undefined behavior. **Best practice**: Always `--dry-run` first for recurring events to verify the generated ICS. --- ## Managing Events ```bash # List upcoming events (next 90 days) $SKILL_DIR/scripts/calendar.sh event list # Search events by text $SKILL_DIR/scripts/calendar.sh event list --search "Allergy" # List with UIDs (for deletion) $SKILL_DIR/scripts/calendar.sh event list --format "{uid} {title}" # Custom date range $SKILL_DIR/scripts/calendar.sh event list --range-start "2026-04-01" --range-end "2026-04-30" # Delete a single (non-recurring) event $SKILL_DIR/scripts/calendar.sh event delete --match "Lunch at Tartine" # Cancel ONE occurrence of a recurring event (adds EXDATE, keeps the series) $SKILL_DIR/scripts/calendar.sh event delete --match "Allergy Shot" --date "2026-03-28" # Delete an entire recurring series (requires --all safety flag) $SKILL_DIR/scripts/calendar.sh event delete --match "Allergy Shot" --all ``` ### Event Delete Safety - Deletes only ONE event at a time. If multiple events match, it lists them and exits. - **Recurring events require `--date` or `--all`**. Without either flag, the tool refuses to act and shows usage. - `--date YYYY-MM-DD`: Adds an EXDATE to skip that one occurrence. The rest of the series continues. - `--all`: Deletes the entire .ics file (the whole series). Use only when the user explicitly wants to cancel all future occurrences. - **NEVER use `rm` on calendar .ics files directly.** Always use `event delete`. - After deleting/cancelling, verify with `event list` or `khal list`. --- ## Managing Contacts Contacts are stored as vCard (.vcf) files in `~/.openclaw/workspace/contacts/default/` and synced to Migadu CardDAV via vdirsyncer. The `send` command validates recipients against this contact list. ```bash # List all contacts $SKILL_DIR/scripts/calendar.sh contact list # Add a contact (single email) $SKILL_DIR/scripts/calendar.sh contact add --name "小鹿" --email "mail@luyx.org" # Add with email type and nickname $SKILL_DIR/scripts/calendar.sh contact add --name "小橘子" --email "Erica.Jiang@anderson.ucla.edu" --type work --nickname "小橘子" # Add a second email to an existing contact $SKILL_DIR/scripts/calendar.sh contact add --name "小橘子" --email "xueweijiang0313@gmail.com" --type home # Delete a contact $SKILL_DIR/scripts/calendar.sh contact delete --name "小橘子" ``` ### Sending to Contacts ```bash # By name (works when contact has a single email) $SKILL_DIR/scripts/calendar.sh send --to "小鹿" ... # By name + type (required when contact has multiple emails) $SKILL_DIR/scripts/calendar.sh send --to "小橘子:work" ... # By raw email (must exist in contacts) $SKILL_DIR/scripts/calendar.sh send --to "Erica.Jiang@anderson.ucla.edu" ... ``` --- ## Replying to Invites ```bash # Accept by himalaya envelope ID $SKILL_DIR/scripts/calendar.sh reply \ --envelope-id 42 \ --action accept # Decline with a comment $SKILL_DIR/scripts/calendar.sh reply \ --envelope-id 42 \ --action decline \ --comment "Sorry, I have a conflict." # From an .ics file $SKILL_DIR/scripts/calendar.sh reply \ --ics-file ~/Downloads/meeting.ics \ --action tentative ``` ### Reply Options | Flag | Required | Description | |-----------------|----------|-----------------------------------------------------| | `--action` | Yes | `accept`, `decline`, or `tentative` | | `--envelope-id` | * | Himalaya envelope ID containing the .ics attachment | | `--ics-file` | * | Path to an .ics file (alternative to `--envelope-id`) | | `--from` | No | Your email (default: `youlu@luyanxin.com`) | | `--account` | No | Himalaya account name | | `--folder` | No | Himalaya folder (default: `INBOX`) | | `--comment` | No | Optional message to include in reply | | `--dry-run` | No | Preview without sending | \* One of `--envelope-id` or `--ics-file` is required. ### Typical Workflow 1. List emails: `himalaya envelope list` 2. Read the invite: `himalaya message read 57` 3. Reply: `$SKILL_DIR/scripts/calendar.sh reply --envelope-id 57 --action accept` --- ## VTODO Tasks Manage tasks as RFC 5545 VTODO components, stored in `~/.openclaw/workspace/calendars/tasks/` and synced to CalDAV. ### Sync Model The agent's local CalDAV is the **source of truth**. When a todo is created, it's saved locally and synced to Migadu CalDAV via vdirsyncer — all connected devices (DAVx5, etc.) pick it up automatically. When the user completes a task, they tell the agent, and the agent runs `todo complete`. The daily `todo check` cron reads from local files. ### Priority Mapping (RFC 5545) | Label | `--priority` | RFC 5545 value | |--------|-------------|----------------| | 高 (high) | `high` | 1 | | 中 (medium) | `medium` | 5 (default) | | 低 (low) | `low` | 9 | ### `todo add` — Create a Todo ```bash $SKILL_DIR/scripts/calendar.sh todo add \ --summary "跟进iui保险报销" \ --due "2026-03-25" \ --priority high \ --description "确认iui费用保险报销进度" \ --alarm 1d ``` | Flag | Required | Description | |-----------------|----------|-----------------------------------------------------| | `--summary` | Yes | Todo title | | `--due` | No | Due date, YYYY-MM-DD (default: tomorrow) | | `--priority` | No | `high`, `medium`, or `low` (default: `medium`) | | `--description` | No | Notes / description | | `--alarm` | No | Reminder trigger: `1d`, `2h`, `30m` (default: `1d`) | | `--dry-run` | No | Preview ICS without saving | ### `todo list` — List Todos ```bash $SKILL_DIR/scripts/calendar.sh todo list # pending only $SKILL_DIR/scripts/calendar.sh todo list --all # include completed ``` ### `todo edit` — Modify a Todo ```bash $SKILL_DIR/scripts/calendar.sh todo edit --match "保险报销" --due "2026-03-26" $SKILL_DIR/scripts/calendar.sh todo edit --uid "abc123@openclaw" --priority high $SKILL_DIR/scripts/calendar.sh todo edit --match "census" --due "2026-03-28" --priority low ``` | Flag | Required | Description | |-----------------|----------|-----------------------------------------------------| | `--uid` | * | Todo UID | | `--match` | * | Match on summary text | | `--due` | No | New due date (YYYY-MM-DD) | | `--priority` | No | New priority: `high`, `medium`, or `low` | \* One of `--uid` or `--match` is required. At least one edit flag must be provided. ### `todo complete` — Mark as Done ```bash $SKILL_DIR/scripts/calendar.sh todo complete --uid "abc123@openclaw" $SKILL_DIR/scripts/calendar.sh todo complete --match "保险报销" ``` ### `todo delete` — Remove a Todo ```bash $SKILL_DIR/scripts/calendar.sh todo delete --uid "abc123@openclaw" $SKILL_DIR/scripts/calendar.sh todo delete --match "保险报销" ``` ### `todo check` — Daily Digest (Cron) ```bash $SKILL_DIR/scripts/calendar.sh todo check ``` Same as `todo list` but only pending items. Exits silently when nothing is pending. --- ## How It Works **Sending invites:** 1. Generates an RFC 5545 ICS file with `METHOD:REQUEST` (via `icalendar` library) 2. Builds a MIME email with a `text/calendar` attachment (via Python `email.mime`) 3. Sends via `himalaya message send` (piped through stdin) 4. Saves the event to `~/.openclaw/workspace/calendars/home/` 5. Runs `vdirsyncer sync` to push to Migadu CalDAV **Replying to invites:** 1. Extracts the `.ics` attachment from the email (via `himalaya attachment download`) 2. Parses the original event with the `icalendar` library 3. Generates a reply ICS with `METHOD:REPLY` and the correct `PARTSTAT` 4. Sends the reply to the organizer via `himalaya message send` (stdin) 5. On accept/tentative: saves event to local calendar. On decline: removes it 6. Runs `vdirsyncer sync` to push changes to Migadu CalDAV **Managing todos:** 1. `todo add`: Creates a VTODO ICS file (via `icalendar` library), saves to `calendars/tasks/`, syncs to CalDAV 2. `todo list/complete/delete/check`: Delegates to `todoman` CLI for robust RFC 5545 VTODO parsing 3. Runs `vdirsyncer sync` after mutations to push changes to Migadu CalDAV **CalDAV sync:** - Events and tasks sync to Migadu and appear on all connected devices (DAVx5, etc.) - Heartbeat runs `vdirsyncer sync` periodically as a fallback - If sync fails, it warns but doesn't block — next heartbeat catches up ## Integration with Email Processor The email processor (`scripts/email_processor/`) may classify incoming calendar invites as `reminder` or `confirmation`. When reviewing pending emails: 1. Check if the email contains a calendar invite (look for `.ics` attachment or "calendar" in subject) 2. If it does, use `reply` instead of the email processor's delete/archive/keep actions 3. The email processor handles the email lifecycle; this skill handles the calendar response ## Checking the Calendar ```bash # List upcoming events (next 7 days) khal list today 7d # List events for a specific date khal list 2026-03-25 # Check for conflicts before sending an invite khal list 2026-03-25 2026-03-26 ``` ## Timezone Reference Common IANA timezones: - `America/Los_Angeles` — Pacific (default) - `America/Denver` — Mountain - `America/Chicago` — Central - `America/New_York` — Eastern - `Asia/Shanghai` — China - `Asia/Tokyo` — Japan - `Europe/London` — UK - `UTC` — Coordinated Universal Time ## Troubleshooting **Invite shows as attachment instead of calendar event?** - Ensure the MIME part has `Content-Type: text/calendar; method=REQUEST` - Some clients require the `METHOD:REQUEST` line in the ICS body **Times are wrong?** - Double-check `--timezone` matches the intended timezone - Use ISO 8601 format: `YYYY-MM-DDTHH:MM:SS` (no timezone offset in the value) **Event not showing on phone/other devices?** - Run `vdirsyncer sync` manually to force sync - Check `~/.openclaw/workspace/logs/vdirsyncer.log` for errors - Verify the .ics file exists in `~/.openclaw/workspace/calendars/home/` **Todos not syncing?** - Check that `~/.openclaw/workspace/calendars/tasks/` exists - Verify vdirsyncer has a `cal/tasks` pair configured - Run `vdirsyncer sync` manually **Recipient doesn't see Accept/Decline?** - Gmail, Outlook, Apple Mail all support `text/calendar` method=REQUEST - Some webmail clients may vary