Agent hallucinated "xiaojuzi@meta.com" instead of looking up the correct address from USER.md. Prompting rules can't reliably prevent this, so recipient validation is now enforced at the tool level. - Add contact list/add/delete subcommands (vCard .vcf files synced via CardDAV) - send --to now resolves through contacts: accepts names, name:type, or known emails; rejects unknown addresses with available contacts shown - Multi-email contacts supported with type qualifier (e.g. "小橘子:work") - Adding contacts and sending are separate operations (no chaining)
445 lines
17 KiB
Markdown
445 lines
17 KiB
Markdown
---
|
|
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
|