Skip to main content

Pipe-friendly calendar CLI for Outlook / Microsoft 365

Project description

owa-cal

Calendar CLI for Outlook / Microsoft 365. Read, create, update and delete events from the terminal. Pipe-friendly JSON by default, --pretty for humans.

brew install damsleth/tap/owa-cal
owa-cal events --pretty

Happy-path setup (no app registration)

owa-piggy owns the token lifecycle; owa-cal just shells out to it on every call. The full first-run flow:

# 1. Install both
brew install damsleth/tap/owa-piggy damsleth/tap/owa-cal

# 2. Seed owa-piggy once from your browser (walks you through it)
owa-piggy setup

# 3. Go
owa-cal events --pretty

Multi-account: seed a named owa-piggy profile and pin it in owa-cal's config.

owa-piggy setup --profile work
owa-cal config --profile work

--profile also works as a one-shot override: owa-cal --profile home events.

Refresh tokens rotate on every call and are persisted by owa-piggy in its own profile store. owa-cal stores no refresh token on this path; owa-piggy --reseed --profile <alias> refreshes the token headlessly when the 24h hard-expiry lapses, and owa-cal picks up the new token on the next call automatically.


The output contract

JSON on stdout, logs on stderr. Every read command emits parseable JSON by default; --pretty is a human override that goes to stdout too. That means the entire CLI composes with jq:

owa-cal events
[
  {
    "id": "AAMkAGI1...redacted",
    "subject": "Standup",
    "start": "2026-04-20T09:00:00",
    "end": "2026-04-20T09:30:00",
    "categories": ["ProjectX"],
    "location": "Teams",
    "showAs": "Busy",
    "isAllDay": false
  },
  {
    "id": "AAMkAGI2...redacted",
    "subject": "Lunsj",
    "start": "2026-04-20T11:00:00",
    "end": "2026-04-20T11:30:00",
    "categories": ["CC LUNCH"],
    "location": "",
    "showAs": "Busy",
    "isAllDay": false
  }
]

Timestamps are normalized to your local timezone. Field names in the output are stable lowercase; the backend is Outlook REST v2 (PascalCase upstream) but owa-cal hides that detail.

owa-cal events | jq '.[].subject'
owa-cal events --date tomorrow | jq '[.[] | select(.showAs == "Busy")] | length'
owa-cal events --week 16 | jq 'group_by(.start | .[0:10]) | map({day: .[0].start[0:10], count: length})'

Same shape on create / update (returns the single normalized event), and on categories (returns [{"name": ..., "color": ...}]).


Commands

owa-cal events --pretty                       # today
owa-cal events --week 16 --pretty             # ISO week
owa-cal events --from 2026-04-14 --to 2026-04-18 --pretty
owa-cal events --search "standup" --pretty

owa-cal create --subject "lunsj" --start 11:00 --end 11:30 --category "CC LUNCH"
owa-cal update --id <event-id> --category "ProjectX"
owa-cal delete --id <event-id>

owa-cal categories                            # JSON
owa-cal categories --pretty                   # aligned table

Auth

Two paths:

  • owa-piggy bridge (default) - owa-cal shells out to owa-piggy, which piggybacks on OWA's public SPA client. No app registration needed; owa-cal stores no refresh token. Optional owa_piggy_profile pins a named owa-piggy profile.
  • With an app registration - set OUTLOOK_APP_CLIENT_ID, OUTLOOK_REFRESH_TOKEN, and OUTLOOK_TENANT_ID in the config file and owa-cal talks to the AAD token endpoint directly.

Config lives at ~/.config/owa-cal/config:

# Default (owa-piggy) path - optional, pins a profile alias
owa_piggy_profile="work"

# App-registration path (optional, mutually exclusive)
OUTLOOK_APP_CLIENT_ID=""
OUTLOOK_REFRESH_TOKEN=""
OUTLOOK_TENANT_ID=""

OUTLOOK_APP_CLIENT_ID can be overridden via the environment. The refresh token / tenant id on the app-registration path live exclusively in the config file.


Dependencies

  • Python 3.8+ (stdlib only - no pip install required at runtime)
  • owa-piggy unless you bring your own app registration

Development

git clone https://github.com/damsleth/cal-cli
cd owa-cal
pip install -e '.[test]'
pytest -q

See AGENTS.md for repo layout and ground rules.

Disclaimer

Personal tooling. Stores a delegated refresh token on disk.
If you don't know why that might be a bad idea, don't use it.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

owa_cal-0.6.0.tar.gz (28.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

owa_cal-0.6.0-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

File details

Details for the file owa_cal-0.6.0.tar.gz.

File metadata

  • Download URL: owa_cal-0.6.0.tar.gz
  • Upload date:
  • Size: 28.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for owa_cal-0.6.0.tar.gz
Algorithm Hash digest
SHA256 bd3fe1dbdfebc55472787e9e3fb5ed64ef945c676b4c469ed07c1a2f6c49bd3c
MD5 4a6fbf8519683a925fba2743403f9fff
BLAKE2b-256 f38780e749550c6c32dc3473290ceb4cba1ac15aca1f0fac3caf28a8be26f364

See more details on using hashes here.

File details

Details for the file owa_cal-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: owa_cal-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 23.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for owa_cal-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7248ed12f7d1abc4dc987a5b33cdcc24a754673012f7a5f8b296601cd95fdc22
MD5 ebbfabe5b60307ff598bfa6791656cd2
BLAKE2b-256 93fa7ef50c8702b937cfd1a586dc7df8491c5023b195164e010fbe16bcd341ac

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page