Private TimeTree bridge for Hermes Agent calendar automation.
hermes-timetree-sync is a small Python CLI and client library that lets Hermes create and sync TimeTree calendar events without asking a chat user to open TimeTree.
TimeTree discontinued its official third-party API in December 2023. This project therefore uses TimeTree's current web endpoints through a deliberately narrow, tested client boundary. It is an internal bridge, not an official TimeTree integration; upstream web-app changes may require maintenance.
The target interaction is simple:
“Add Day off on May 18.”
Hermes should translate that request into a TimeTree write using locally stored credentials. No browser, TimeTree UI, cookie copying, or OAuth prompt should appear during normal chat usage.
| Area | Current support |
|---|---|
| Auth | Stored TimeTree web session cookie; experimental email/password exchange |
| Discovery | List calendars and labels |
| Reads | Sync calendar events through TimeTree's web sync endpoint |
| Writes | Create, update, and delete events through guarded client methods |
| Hermes UX | Non-interactive all-day event creation, batch writes, and Safari-backed timed event creation |
| Labels | Optional local YAML policy for mapping terms/categories to TimeTree labels |
| Safety | Redacted docs/tests, low-volume API usage, mocked HTTP coverage |
git clone git@github.com:kfa-ai/hermes-timetree-sync.git
cd hermes-timetree-sync
uv sync --devRun the local quality gates:
uv run pytest
uv run ruff check .Provide runtime configuration via .env or environment variables:
TIMETREE_SESSION_COOKIE=...
TIMETREE_CALENDAR_ID=...TIMETREE_SESSION_COOKIEis the value of TimeTree's_session_idbrowser cookie. Treat it as a bearer secret.TIMETREE_CALENDAR_IDis the target TimeTree calendar ID for writes.
For local email/password TimeTree accounts, an experimental sign-in command can attempt to exchange credentials for a web session:
TIMETREE_EMAIL=you@example.com
TIMETREE_PASSWORD=...uv run hermes-timetree-sync sign-inDirect sign-in may fail depending on TimeTree's browser/session checks. Production Hermes usage should rely on a stored, locally refreshed session cookie. See docs/authentication.md.
Check configuration:
uv run hermes-timetree-sync doctorList calendars:
uv run hermes-timetree-sync list-calendarsCreate one all-day event:
uv run hermes-timetree-sync create-all-day --title "Day off" --date 2026-05-18Create several all-day events with one current-user lookup:
uv run hermes-timetree-sync create-all-day-batch \
--event "2026-05-18|Day off" \
--event "2026-05-25|Public holiday"The batch command was added in v0.1.1 for faster Hermes calendar writes. It also includes regression coverage for TimeTree accounts whose /api/v1/user ID is returned as a number rather than a string.
Create one timed event through Safari's authenticated TimeTree page context:
uv run hermes-timetree-sync create-timed \
--title "Lynsey out" \
--start 2026-05-24T12:00 \
--end 2026-05-24T18:00 \
--category lynseycreate-timed requires Safari to already be logged into TimeTree with Develop → Allow JavaScript from Apple Events enabled. It opens the target TimeTree calendar, posts a timed all_day: false event with browser credentials: include, then verifies the created event through /events/sync. This path exists because direct non-browser writes with only _session_id can return HTTP 422 even when browser-context writes succeed.
This package is intentionally UI-free at runtime:
- A local setup/bootstrap step stores or refreshes
TIMETREE_SESSION_COOKIEandTIMETREE_CALENDAR_IDoutside chat. - Hermes parses a natural-language calendar request into structured event data.
- Hermes calls this CLI/client directly.
- For all-day events, the TimeTree UI is not opened during the user request. Timed events currently use Safari page-context execution because the direct replay path is unreliable for writes.
For multiple requested all-day events, Hermes-facing wrappers should prefer create-all-day-batch so the current TimeTree user is fetched once and reused for all event attendees. For timed events, wrappers should prefer create-timed rather than embedding Safari JavaScript locally.
Create/update helpers can set label_id from a local YAML policy instead of hard-coding calendar-specific terms in source code.
cp timetree-labels.yaml.example timetree-labels.yamlEdit timetree-labels.yaml with the target calendar's label IDs and matching terms. The local file is ignored by git.
To use a different policy file:
export TIMETREE_LABEL_POLICY_FILE=/path/to/timetree-labels.yamlNever commit or paste into chat:
- TimeTree passwords;
_session_id/_timetree_sessioncookies;- raw private calendar payloads;
- Google OAuth tokens or other downstream calendar credentials.
Local .env files are for development/runtime configuration only. Keep examples, docs, tests, logs, and issue comments sanitized.
docs/authentication.md— session-cookie, sign-in, and non-interactive runtime guidance.docs/reverse-engineered-api.md— observed TimeTree web endpoints and payload notes.docs/roadmap.md— likely next steps and maintenance considerations.CHANGELOG.md— release history.
When adding endpoint coverage:
- keep TimeTree-specific behavior behind
TimeTreeClientor narrow CLI helpers; - prefer mocked HTTP tests over live credentials;
- redact
_session_id, passwords, cookies, and raw private calendar data; - run
uv run pytestanduv run ruff check .before publishing changes.
Private/internal project unless a license is added later.