@infaris/calendar-cli
v0.9.0
Published
Command line interface for Calendar by Infaris: log in once on your device, read your agenda, manage work tasks, and fill the urenstaat from tasks, commits and PRs.
Readme
@infaris/calendar-cli
Command line interface for Calendar. Log in once on your device with a browser (or have an AI agent relay the sign-in through chat, see below), then read your agenda, find free time, create/move/delete events, manage work tasks (with image attachments), stage mail concepts and fill the urenstaat from your terminal (or from tools like Claude Code, Codex, OpenCode or Cursor).
Built for AI assistants: every read command supports --json, accepts relative
dates ("today", "tomorrow", "+3d", "tomorrow 09:00"), and the agenda expands
recurring events into concrete occurrences so an agent can answer "what's on my
calendar?" without understanding RRULEs.
It also respects your Calendar settings: new events land on your default
calendar and use your default duration, free uses your configured working
hours, weekend visibility and week start. Override any of these with flags.
Install
npm install -g @infaris/calendar-cliThis installs infaris-calendar and infaris-crm (customer dossiers, same login).
Log in
infaris-calendar loginThis opens your browser, signs you in through Infaris (OAuth with loopback
PKCE, same flow as the Env CLI CLI), and stores tokens in
~/.infaris-calendar/credentials.json. Only Infaris admin accounts can use the
CLI (Calendar is an internal tool), and your account must have signed in
to the Calendar web app at least once.
Access tokens are short-lived and refreshed automatically via the stored refresh token; you only need to log in again when the refresh token expires (30 days of inactivity).
Signing in over SSH / a remote session (e.g. t3code)
The default flow opens a browser on the same machine that runs the CLI and waits for Infaris to redirect back to a local port - that only works when the CLI and the browser are on the same box. When you're on a remote/SSH session, run:
infaris-calendar login --manual(an SSH session is auto-detected via $SSH_CONNECTION/$SSH_TTY, so most of
the time you don't even need the flag). Instead of a local callback, it prints
a URL to open on any device - your Windows laptop, phone, whatever has a
browser. After you sign in there, Infaris shows your code on a page with a
Copy button. Paste it back into the terminal.
Signing in through an AI agent (Claude Code, Codex, OpenCode, Cursor)
login --manual needs one process to stay open waiting for your paste, which
doesn't work when an agent is relaying the URL and code through a chat instead
of a live terminal. For that, the sign-in is split into two separate commands:
infaris-calendar login link # 1. prints a URL, then exits immediately
infaris-calendar login code <value> # 2. run later, once you have the code
printf '%s' "$CODE" | infaris-calendar login code - # same, keeps the code out of argv/history
infaris-calendar login status --json # signed-in account + pending link, never tokensRe-running login link prints the same URL while the pending sign-in still has
more than 5 minutes left, so an agent retry never invalidates the link you are
completing; login link --new forces a fresh one. Token refreshes are locked
across processes, so parallel agents sharing one home don't rotate the same
refresh token twice (Infaris revokes the whole token family on reuse). Without
a terminal, login --manual fails immediately with the login link hint
instead of hanging.
login link stores the pending PKCE state under ~/.infaris-calendar/ (expires
after 15 minutes) and exits, so an agent can post the printed URL in the chat;
you open it on your own device, sign in, and Infaris shows your code with a
Copy button. Paste that code back to the agent, which runs
login code <value> to finish. A wrong or expired paste doesn't invalidate the
pending sign-in - rerun login code with the right value without starting
over, as long as you're still inside the 15-minute window.
To point at a non-production instance:
infaris-calendar login --url http://localhost:3000
# or set it for the whole session:
export INFARIS_CALENDAR_URL=http://localhost:3000
# the OAuth issuer can be overridden too:
export INFARIS_CALENDAR_ISSUER=https://infaris.comRead your agenda
infaris-calendar agenda # today
infaris-calendar agenda --tomorrow
infaris-calendar agenda --week # current week (Mon-Sun)
infaris-calendar agenda --month
infaris-calendar agenda --days 7 # next 7 days
infaris-calendar agenda --from 2026-07-01 --to 2026-07-07
infaris-calendar agenda --calendar <id> # one calendar only
infaris-calendar agenda --week --json # full details for an AI assistantCalendars you've turned off (hidden) in Calendar are excluded, just like in
the web app. Pass --include-hidden to show them, or --calendar <id> to target
one specific calendar regardless of its visibility.
Recurring events are expanded into individual occurrences within the range by
default (pass --no-expand to get the raw master events with their RRULE).
--json includes every detail: description, location, attendees, status,
timezone, colour, the calendar id and name, and recurringEventId/isOccurrence
for expanded occurrences.
list events is an alias for agenda.
Agenda options:
| Flag | Description |
| --- | --- |
| --from <date> | Range start (ISO, or today/tomorrow/+3d) |
| --to <date> | Range end (inclusive) |
| --days <n> | Number of days from --from or today |
| --today / --tomorrow | Single day (today is the default) |
| --week / --month | Current week (Mon-Sun) / calendar month |
| --calendar <id> | Only events from this calendar id |
| --search <text> | Filter by title, description or location |
| --include-hidden | Also include calendars you turned off |
| --no-expand | Do not expand recurring events |
| --json | Output as JSON |
List your calendars
Your personal calendar, location/shared calendars and any connected Google/ iCloud/Microsoft/CalDAV accounts:
infaris-calendar calendars
infaris-calendar calendars --json
infaris-calendar calendars --set-default "Arjan"Use the printed calendar id with agenda --calendar and free --calendar.
create event --calendar, update event --calendar and --set-default accept
either an exact calendar name or an id and only select a writable calendar.
Create a calendar event
infaris-calendar create event --title "Lunch with client" \
--start "tomorrow 12:00" --duration 60 --location "Café Central, Amsterdam"
infaris-calendar create event -t "Sprint review" \
--start 2026-07-01T14:00 --end 2026-07-01T15:00 --calendar <id>
infaris-calendar create event -t "Day off" --start 2026-07-04 --all-day
infaris-calendar create event -t "Conference" \
--start 2026-10-19 --end 2026-10-20 --all-day \
--calendar "Arjan" --attendee Michan NoahNew timed events that overlap something already on your calendar print a
non-blocking warning (turn it off with --no-conflict-check).
| Flag | Description | Default |
| --- | --- | --- |
| -t, --title | Event title (required) | — |
| -s, --start | Start (ISO or relative, required) | — |
| -e, --end | End (ISO or relative). With --all-day, the date is inclusive | start + duration |
| --duration <minutes> | Used when --end is omitted | your settings (60) |
| -a, --attendee <people...> | Invite team members by first name, full name or email | none |
| --all-day | All-day event | — |
| -l, --location | Location | — |
| -d, --description / --description-file | Description (file - = stdin) | — |
| --calendar <id> | Target calendar | your default calendar |
| --no-conflict-check | Skip the overlap warning | — |
| --json | Output the created event as JSON | — |
Your next event
infaris-calendar next # next 3 upcoming events
infaris-calendar next --limit 1 # just the next one
infaris-calendar next --days 7 --jsonFind free time
Find open slots within working hours, in your local timezone. All-day events (e.g. holidays) count as busy by default. Weekends are skipped unless you ask for them.
infaris-calendar free --tomorrow # gaps tomorrow
infaris-calendar free --week --min 60 # 1h+ slots this week
infaris-calendar free --days 5 --work-start 08:00 --work-end 18:00
infaris-calendar free --week --json # for an AI to pick a slot| Flag | Description | Default |
| --- | --- | --- |
| range flags | Same as agenda (--today/--week/--days/--from...) | today |
| --work-start <HH:mm> / --work-end <HH:mm> | Working hours | your settings (09:00 / 17:00) |
| --min <minutes> | Minimum slot length to report | 30 |
| --include-weekends | Include Saturday and Sunday | your settings |
| --ignore-all-day | Don't treat all-day events as busy | off |
| --calendar <id> | Only consider this calendar as busy | all |
| --json | Output slots as JSON | — |
Show, reschedule or delete an event
Get the event id from agenda --json (or next --json), then:
infaris-calendar show event <id> # full details
infaris-calendar show event <id> --json
infaris-calendar update event --id <id> --start "tomorrow 15:00" --end "tomorrow 16:00"
infaris-calendar update event --id <id> --shift 15 # move 15 min later (-15 = earlier)
infaris-calendar update event --id <id> --title "New title" --location "Room 2"
infaris-calendar update event --id <id> --attendee Michan Noah
infaris-calendar update event --id <id> --clear-attendees
infaris-calendar delete event --id <id>For recurring events the change applies to the whole series (the CLI tells you when that's the case).
| update event flag | Description |
| --- | --- |
| --id <id> | Event id (required) |
| -t, --title / -l, --location / -d, --description | Edit fields |
| -s, --start / -e, --end | New start/end (ISO or relative) |
| --shift <minutes> | Move the whole event by N minutes (+later, -earlier) |
| --calendar <id> | Move to another calendar |
| -a, --attendee <people...> | Replace attendees by first name, full name or email |
| --clear-attendees | Remove all attendees |
| --json | Output the updated event as JSON |
Create a task
infaris-calendar create task \
--title "Agent: collect disk I/O metrics" \
--product pulseguard \
--priority medium \
--type development \
--category feature_request \
--description "Add per-disk read/write throughput and IOPS to the agent."Long descriptions can be read from a file (or stdin with -):
infaris-calendar create task --title "..." --product pulseguard --description-file ./task.md
cat task.md | infaris-calendar create task --title "..." --description-file -Options:
| Flag | Description | Default |
| --- | --- | --- |
| -t, --title | Task title (required) | — |
| -p, --product | general, pulseguard, pulsefiles, pulseqr, pulsecalendar, pulsesupport, pulsemail, ipulse | pulseguard |
| -d, --description | Markdown description | — |
| --description-file | Read description from a file (- = stdin) | — |
| --priority | high, medium, low | medium |
| --status | not_started, in_progress, done | not_started |
| --type | one or more of general, development, marketing | development |
| --category | one or more of bug, refinement, feature_request | — |
| --relation-number | linked customer relation number | — |
| --due | due date (ISO) | — |
| --image <paths...> | attach image(s) or a PDF (max 10 MB each) | — |
| --json | print the created task as JSON | — |
Attach a file to a task
Add an image or PDF to a task after the fact (creating a task already accepts
--image directly):
infaris-calendar attach task --id 427 ./screenshot.png
infaris-calendar attach task --id 427 ./screenshot.png ./logs.pdf10 MB per file, same limit as the web app's attachment upload.
Stage a mail draft
Prepares an editable concept in your own webmail; it never sends anything.
Open the printed URL to review and send it from /dashboard/mail.
infaris-calendar mail draft --to [email protected] --subject "Offerte" \
--body "<p>Hoi,</p><p>Zie bijlage.</p>" --attach ./offerte.pdf
infaris-calendar mail draft --to [email protected] --subject "Screenshot" \
--body-file ./mail.html --from [email protected] --json| Flag | Description |
| --- | --- |
| --to <email> | Recipient (required) |
| --subject <text> | Subject |
| --body <html> / --body-file <path> | HTML body (file - = stdin) |
| --from <email> | Preferred sender mailbox address |
| --attach <path> | Image or PDF to attach (max 10 MB) |
| --json | Output the draft id/url as JSON |
List tasks
infaris-calendar list tasks
infaris-calendar list tasks --status in_progress
infaris-calendar list tasks --search agent --include-done
infaris-calendar list tasks --assignee me --json
infaris-calendar list tasks --jsonUrenstaat
Fill https://calendar.infaris.com/dashboard/hours from Calendar tasks, git
commits (IF-687 / #687 in the subject; legacy IP-687 also works) and GitHub PRs for that day.
Needs infaris-calendar login, a git checkout, and optionally gh auth login.
infaris-calendar hours # today
infaris-calendar hours --week
infaris-calendar hours fill # draft from tasks + commits + PRs
infaris-calendar hours fill --date yesterday --json
infaris-calendar hours fill --date 2026-09-02 --write
infaris-calendar hours fill --date yesterday --write --replace # overwrite
infaris-calendar hours log --date today --hours 8 --notes "#687 uren-skill"
infaris-calendar hours delete --date todayhours fill writes nothing until you pass --write. If that day already has a
row, it refuses unless you also pass --replace. Hours default to your
weekday default (8u on a normal weekday). Notes are one summarised line:
Guard / Calendar / Env / reviews / meetings, with task keys in parentheses.
Other commands
infaris-calendar whoami # show the logged-in account
infaris-calendar logout # remove the stored tokenHow auth works
The CLI is a public OAuth client (pulsecalendar-cli, a server-side registration) on the Infaris identity
provider, using authorization code + PKCE with a loopback redirect (same
pattern as gh auth login and the Env CLI CLI):
- The CLI starts a one-shot HTTP server on
127.0.0.1:47655. - It opens
https://infaris.com/api/oauth/authorizein your browser with a PKCE challenge. - Infaris signs you in and redirects back to
http://127.0.0.1:47655/callbackwith an authorization code. - The CLI exchanges the code (plus PKCE verifier) for access and refresh tokens and stores them locally.
The Calendar API verifies these RS256 tokens against the Infaris JWKS and
maps your Infaris account to your Calendar user by e-mail. New tasks are
assigned to you by default, with priority medium, type development, status
not_started and the current sprint, so AI agents can create a task with just
a --title.
Develop
npm install
npm run dev -- create task --title "test" --product pulseguard # run from source via tsx
npm run build # emit dist/Customer dossiers and complete ticket context (0.8.0)
This package now also installs infaris-crm, using the same login as
infaris-calendar. Read the customer support workflow
for documentation ownership, daily work, handoff and rollout.
infaris-crm list --search "Customer" --json
infaris-crm create --name "Customer" --website https://example.com --json
infaris-crm show RELATION --include-done --json
infaris-crm contact RELATION --name "Person" --email [email protected]
infaris-crm note RELATION --method PHONE --outcome SPOKEN --body-file call.md
infaris-crm ticket RELATION --title "Slow website" --description-file problem.md --image screenshot.png --json
infaris-calendar list tasks --relation-number RELATION --json
infaris-calendar show task 427 --customer-context --download-dir /tmp/ticket-427 --json
infaris-calendar comment 427 --body-file investigation.md --json
infaris-calendar update task --id 427 --assignee me --status in_progress
infaris-calendar update task --id 427 --clear-assignees
infaris-calendar link-pr 427 https://github.com/InfarisHQ/monorepo/pull/123 --jsonTask creation also accepts --assignee <people...> by team name, email, id or
me. show task includes description, comments, subtasks, PRs and attachment
URLs. --customer-context adds the linked CRM dossier. --download-dir downloads
attachments without overwriting existing files. Open images with your agent's
image viewer; do not infer their contents from filenames.
CRM commands return JSON by default. list supports --limit and --offset. Customer history defaults to the latest
50 entries; entriesPagination reports the total and next offset. Read older
entries with show --entries-offset <n> and --entries-limit <n> (max 200).
contact --id replaces that contact's details, so include fields you want to
keep. note appends; it never overwrites the history. create creates a prospect
without billing or a Guard license. Check returned warning values before
retrying. Ticket creation reports its task number if a later upload fails;
retry with attach task, not a second ticket.
Use infaris-crm login, or login link / login code <value> for remote sessions.
The logout affects both CLI tools. INFARIS_CALENDAR_CONFIG_DIR can select an
isolated login directory for a separate profile or automation session. Override the CRM server with INFARIS_CRM_URL;
a non-production Calendar login requires this explicitly. Support must deploy
the /api/crm/* routes before CRM commands are available.
