Skip to content

clawdbotatg/clawd-calendar

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

clawd-calendar

Self-hosted scheduling — a simpler, rule-driven, open-source Calendly replacement. One page, three password tiers, Google Calendar invites. Zero npm dependencies (Node ≥ 22.5: node:http, node:sqlite, fetch).

image

Guests follow a link (or type a password), see open slots in their own timezone, pick one, enter name + email — the event lands on the owner's Google Calendar with the guest as attendee, and Google sends the invite email (with Meet link, accept/decline, the works). No SMTP.

Fork it — everything is env config

All personalization lives in one gitignored env file. To run your own:

git clone <your-fork> && cd clawd-calendar
cp .env.example .clawd-calendar.env   # edit: your name, timezone, hours, rules
node scripts/auth.js                  # one-time Google OAuth (fills in the secrets)
node scripts/mint.js "friends" --token <pick-a-password>
node server.js

Nothing in the repo is personal — no code edits needed. Every knob (CAL_OWNER_NAME, CAL_OWNER_TZ, availability windows, slot shape, buffers, daily cap, event title…) is documented in .env.example; real env vars override the file.

Personal images live in a gitignored assets/ folder served on top of public/ — drop in avatar.jpg (set CAL_AVATAR_URL=/avatar.jpg), favicon-32.png, apple-touch-icon.png, and an og.png (1200×630 unfurl card) and they're picked up at those URLs without being committed.

The rules engine

Slots are generated by one pure function (lib/slots.js), governed by env config:

  • Window — e.g. weekdays 8:00–17:00 in America/Denver (DST-safe).
  • Busy subtraction — owner's Google free/busy removes conflicts.
  • Prep buffer — e.g. 15 min needed before each slot (checked against busy events, not the window edge — 8:00 stays bookable even though prep would be 7:45).
  • Slot shape — e.g. 60-min calls on 30-min start increments.
  • Daily cap — one call per day; a booked day disappears. The calendar is the source of truth: delete a booked event from Google Calendar and its day reopens (the local booking is auto-cancelled), and for types with a fixed event title (e.g. SLOP.COMPUTER) any calendar event with that name in its title or description — even one you added by hand — spends the day (Prepare: … companion blocks don't count).
  • Min notice / max days out.
  • Book-time re-check — rules re-run for the exact slot before the event is created; a race loses gracefully with "slot just taken".

Password tiers

The password is the token (/a/<password>, or typed on the landing page):

tier window busy daily cap
standard config window subtracted enforced
vip any day, wide hours subtracted bypassed
override any day, wide hours ignored — owner moves things bypassed

Mint them from the CLI (--token sets a memorable password, otherwise random):

node scripts/mint.js "friends"  --token <password>
node scripts/mint.js "Vitalik"  --tier vip
node scripts/mint.js "surprise guest" --tier override --max-uses 1
node scripts/mint.js "podcast" --duration 90 --days sat,sun --start 09:00 --end 12:00
node scripts/mint.js --list

Per-token overrides (window/duration/cap) work on any tier.

No-password ("public") mode — mint a link with --public and its route works bare: node scripts/mint.js "walk-ins" --public --duration 30 opens / itself for booking; add --type slop and /slop opens instead. The public token is still a normal link (tier, overrides, use counts) — if it gets abused, disable it (CLI or admin page) and the password gate is back. A wrong password never falls back to the public token.

Event types (different kinds of links)

Beyond the default call, each event type is its own route (/<key>/<password>) with its own landing style and rules — duration, weekly window, per-type daily cap (office hours filling up doesn't consume the one-call-per-day budget), min notice, calendar event title, page title/description, and an accent color that re-themes the whole page. A type can also customize the calendar invite itself: custom description text (--event-desc), a location (--location), Meet link on/off (--meet/--no-meet), and a prep block (--prep 15 — an owner-only "Prepare: …" event created on the calendar right before each booking; the slot search keeps that gap clear of busy events). For a fully custom look a type can carry a per-type avatar (--avatar), an ASCII banner rendered above the picker (--ascii-file), and a named skin (--skin) — a CSS theme in index.html (e.g. slop). Anything unset inherits the env config. Resolution chain: env config → event type → token (tiers still work inside any type).

node scripts/types.js "Office Hours" --key office --duration 30 \
     --days fri --start 14:00 --end 16:00 --daily-cap 4 \
     --accent "#f5a623" --desc "Drop-in office hours — 30 min, Fridays." \
     --event-title "Office hours: {name}"
node scripts/mint.js "students" --type office --token <password>   # → /office/<password>
node scripts/types.js --list
node scripts/types.js --disable office   # kills all its links (--enable undoes)

A password typed on the landing page finds its own event type — the page redirects itself to the right route.

Admin page

Set CAL_ADMIN_PASSWORD and /admin/<password> becomes a one-page admin: upcoming + past bookings, every link (copy/disable/enable, use counts), and event types (create, disable, mint a link straight into one). It does the same things scripts/types.js / scripts/mint.js do over ssh — use whichever is closer. Unset (the default), the route doesn't exist.

Google OAuth prereq (one-time, ~5 min)

node scripts/auth.js walks you through it (full steps in the script header): create a Google Cloud project → enable the Calendar API → OAuth consent screen (External, then Publish so the refresh token never expires) → create a "Desktop app" OAuth client → paste the id + secret. Secrets land in .clawd-calendar.env; the server also falls back to ~/.config/gcal-skill/credentials.json. SQLite db lives in data/ (override with CAL_DB).

Dev / test

npm run dev    # GCAL_FAKE=1 — canned busy blocks, fake event creation
npm test       # rules-engine test suite (node --test)

Events created by the tool are tagged extendedProperties.private.scheduler=true on Google Calendar; the local bookings table is the daily-cap fast path and audit log. Google Calendar stays the source of truth for busy time.

Roadmap

  • Phase 2 — optional guest calendar overlay (Google OAuth, calendar.freebusy scope only) to grey out mutually-busy slots.
  • Phase 3tiny admin page (mint links, upcoming bookings) (shipped), reminders, first-party cancel/reschedule.
  • Phase 4 — voice agent: same getOpenSlots()/book endpoints, third client.

About

Self-hosted scheduling — a simpler, rule-driven, open-source Calendly replacement. Zero npm dependencies.

Resources

Stars

14 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors