Metadata-Version: 2.5
Name: blumkin
Version: 1.2.1
Summary: Personal Microsoft 365 / Graph skills CLI (delegated as me)
Project-URL: Changelog, https://github.com/the-hcma/blumkin/blob/main/CHANGELOG.md
Project-URL: Homepage, https://github.com/the-hcma/blumkin
Project-URL: Issues, https://github.com/the-hcma/blumkin/issues
Project-URL: Repository, https://github.com/the-hcma/blumkin
Author-email: Henrique Andrade <thehcma@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: blumkin,cli,microsoft-graph,outlook,teams
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Communications :: Email
Requires-Python: >=3.14
Requires-Dist: azure-identity>=1.25.3
Requires-Dist: click>=8.5.0
Requires-Dist: google-api-python-client>=2.200.0
Requires-Dist: google-auth-httplib2>=0.4.2
Requires-Dist: google-auth-oauthlib>=1.4.1
Requires-Dist: google-auth>=2.57.1
Requires-Dist: httplib2>=0.32.0
Requires-Dist: icalendar>=7.3.0
Requires-Dist: msal>=1.38.0
Requires-Dist: msgraph-sdk>=1.62.0
Requires-Dist: python-docx>=1.2.0
Requires-Dist: tomlkit>=0.15.1
Provides-Extra: dev
Requires-Dist: openpyxl>=3.1; extra == 'dev'
Requires-Dist: pdfplumber>=0.11; extra == 'dev'
Requires-Dist: pyright>=1.1.411; extra == 'dev'
Requires-Dist: pytest>=9.1.1; extra == 'dev'
Requires-Dist: ruff>=0.16.6; extra == 'dev'
Provides-Extra: mcp
Requires-Dist: mcp>=2.1.1; extra == 'mcp'
Provides-Extra: ocr
Requires-Dist: pdf2image>=1.17; extra == 'ocr'
Requires-Dist: pillow>=11; extra == 'ocr'
Requires-Dist: pytesseract>=0.3; extra == 'ocr'
Provides-Extra: pdf
Requires-Dist: pdfplumber>=0.11; extra == 'pdf'
Provides-Extra: xlsx
Requires-Dist: openpyxl>=3.1; extra == 'xlsx'
Description-Content-Type: text/markdown

# blumkin

[![PyPI version](https://img.shields.io/pypi/v/blumkin.svg)](https://pypi.org/project/blumkin/)
[![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-blue.svg)](https://pypi.org/project/blumkin/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/the-hcma/blumkin/blob/main/LICENSE)
[![CI](https://github.com/the-hcma/blumkin/actions/workflows/ci.yml/badge.svg)](https://github.com/the-hcma/blumkin/actions/workflows/ci.yml)
[![Release Please](https://github.com/the-hcma/blumkin/actions/workflows/release-please.yml/badge.svg)](https://github.com/the-hcma/blumkin/actions/workflows/release-please.yml)

Personal **Microsoft 365 and Google Workspace** skills CLI — named after Rose “Mrs. B” Blumkin, Berkshire’s legendary operator.

blumkin turns your own calendar, mail, and chat into **small, invokable skills** any coding agent (**Cursor**, **GitHub Copilot**, **Claude**, …) — or a human — can run over the shell, instead of re-implementing OAuth and the Graph / Google API clients each time.

It acts **as the signed-in user** over **delegated** OAuth (public client + interactive browser). No server, no app-only permissions, no shared secret. `--json` on every command for agents.

## What it does

One config file, one or more **named profiles** (Microsoft, Google, or both), selected with `--profile` or a tag. `blumkin skills list --json` is the live catalog:

| Group | Skills |
|-------|--------|
| `auth` | `login`, `logout`, `refresh`, `status` |
| `calendar` | `today`, `view`, `freebusy`, `suggest`, `create`, `accept`, `cancel`, `update` |
| `mail` | `inbox`, `list`, `get`, `folders`, `draft`, `update-draft`, `delete-draft`, `send-draft`, `reply`, `forward`, `signature`, `attachments` (+ `download`) |
| `chat` (Teams / Google Chat) | `find`, `last`, `send`, `edit`, `delete`, `attachments` (+ `download`) |
| `docs` | `create` (a Markdown subset → a real Google Doc, or a `.docx` in OneDrive) |
| `meeting` (Microsoft) | `get`, `transcription` |
| `people` | `resolve` |
| `mcp` | `serve`, `install`, `status` |

Plus `blumkin doctor` (setup check), `skills` / `profiles` (discovery), `upgrade` (self-update via pipx / uv tool / an editable checkout), and `completion`.

Reads work with the base scope set. Anything that reaches another person (mail send, calendar invite, chat message) needs an explicit `--yes`. Google support is at near-parity with Microsoft — the **Google Workspace** section below has the exact verb list and the handful of provider differences.

## Install (`blumkin` on `PATH`)

```bash
pipx install blumkin
pipx ensurepath          # first pipx install only; opens a new shell
```

Then invoke the binary directly — **not** `uv run blumkin`:

```bash
blumkin --version                       # version, commit, and which binary answered
blumkin auth login                      # once per machine / when cache is cold
blumkin doctor                          # config, token cache, active scope set
blumkin skills list --json              # machine-readable catalog for agents
blumkin calendar today --json
blumkin mail inbox --top 10 --json
blumkin chat last --with "Sam Rivera" --n 3 --json
```

`pipx` puts `blumkin` in its bin dir (usually `~/.local/bin`); `pipx ensurepath`
makes sure that is on `PATH`.

### Upgrade

```bash
blumkin upgrade
```

Detects how `blumkin` is installed and acts to match: `pipx upgrade blumkin` for
a pipx app, `uv tool upgrade blumkin` for a uv tool, and for an editable
`-e <path>` install (or a bare source checkout) the `git pull --ff-only` plus
`--force` reinstall a package upgrade cannot do — printed to run yourself, or run
for you with `blumkin upgrade --yes`. `from:` / `to:` are the on-disk
`blumkin --version` before and after; an unmanaged install is reported, not
touched.

### From a clone (developing blumkin)

```bash
uv sync --group dev
uv tool install -e .        # editable; `blumkin` now points at the checkout
```

`blumkin --version` reports the checkout's commit. `blumkin upgrade` recognises
the editable install and prints (or, with `--yes`, runs) `git pull --ff-only`
followed by `uv tool install -e . --force` to re-bake the metadata; `blumkin
doctor` warns when a pull left the installed version stale.

To expose every skill as a typed **MCP tool** for MCP-aware agents, install the
`mcp` extra (`pipx install 'blumkin[mcp]'`) and run **`blumkin mcp install`** — a
guided setup that registers `blumkin mcp serve` with Claude Code, Cursor, and the
GitHub Copilot CLI, confirming each. See
[`docs/agent-integration.md`](./docs/agent-integration.md#mcp-server).

To use blumkin from agent sessions in **other** repos (Cursor personal skill, or
Copilot CLI instructions), see [`docs/agent-integration.md`](./docs/agent-integration.md).
For cutting a release, see [`docs/RELEASING.md`](./docs/RELEASING.md).

### Discovering commands

Every group and leaf command has `--help` with a description and worked
examples:

```bash
blumkin --help                     # top-level map, common workflows, exit codes
blumkin calendar --help            # a group's commands + typical flows
blumkin calendar create --help     # one command: args, notes, example invocations
```

`blumkin skills list --json` is the machine-readable catalog for agents.

### Shell completion

`blumkin completion <bash|zsh|fish>` prints a completion script. Let blumkin
install it to the per-user completion dir (idempotent; `--force` to overwrite,
`--json` reports the path):

```bash
blumkin completion bash --install
blumkin completion zsh --install    # then put its dir on $fpath before compinit
blumkin completion fish --install
```

Or place it yourself — bash-completion v2 lazy-loads this path (no rc edit):

```bash
blumkin completion bash > ~/.local/share/bash-completion/completions/blumkin.bash
blumkin completion fish > ~/.config/fish/completions/blumkin.fish
```

…or source it from your rc file:

```bash
blumkin completion bash > ~/.blumkin-complete.bash
echo 'source ~/.blumkin-complete.bash' >> ~/.bashrc
blumkin completion zsh > ~/.blumkin-complete.zsh
echo 'source ~/.blumkin-complete.zsh' >> ~/.zshrc
```

Open a new shell afterwards. The script calls back into `blumkin` for
completions, so keep it on `PATH`.

## Config (`~/.config/blumkin/`)

Create `~/.config/blumkin/config.toml` (mode `0600`). One file, **named
profiles** — one per account, Microsoft or Google — selected with `--profile`,
a `tags` entry, or `BLUMKIN_PROFILE` (see `blumkin profiles list --json`). With
more than one profile, blumkin fails closed rather than guessing which account
to act as.

```toml
default_profile = "work"

[profiles.work]
client_id = "<entra-public-client-id>"
tenant_id = "<your-entra-tenant>"
default_tz = "<IANA timezone, e.g. America/New_York>"
provider = "microsoft"
tags = ["@work", "work", "microsoft", "m365"]

[profiles.personal]
provider = "google"
default_tz = "<IANA timezone>"
google_oauth_client_file = "~/path/to/google-oauth-desktop-client.json"
tags = ["@personal", "personal", "google", "gmail"]
```

`config.toml` must use `[profiles.<name>]`; flat top-level keys are not
supported (a config with none configures zero profiles rather than an implicit
one).

A top-level `[preferences]` table sets `font_name` / `font_size` / `html_email`
(default `true`) for every profile; `[profiles.<name>.preferences]` overrides
one profile. A profile value that disagrees with the top-level one for the same
key still wins, but blumkin warns about it on stderr:

```toml
[preferences]
font_name = "Calibri"
font_size = 11
html_email = true

[profiles.personal.preferences]
font_size = 13  # overrides just this key for this profile
```

Set `tenant_id`, `default_tz`, and `provider` in the profile table (there are no
org-specific code defaults). `provider` defaults to `microsoft` when omitted.

Interactive browser auth is public-client only (`client_id`; plus `tenant_id` for
Microsoft). Do not set a client secret for these flows.

Microsoft token cache files (under `profiles/<name>/`):

- `msal_token_cache.json`
- `auth_record.json`

`BLUMKIN_CONFIG_DIR` overrides the config **directory** (not a profile) — useful
for an isolated setup or tests. Never commit anything under the config dir.

### Google Workspace (`provider = "google"`)

**Full walkthrough:** [`docs/google-setup.md`](./docs/google-setup.md)
(Console project, APIs, consent screen / test users, Desktop client JSON,
named profile, login, smoke, troubleshooting).

Short form — point the profile at your Google Cloud **Desktop** OAuth client
JSON (the Console download). That file holds `client_id` / `client_secret`; do
not put the secret in toml or environment variables. **Download that JSON when
you create the client** — the secret is shown once and Google will not let you
re-download it later (recovery means Reset secret / a new client; see
[`docs/google-setup.md`](./docs/google-setup.md) §A.4):

```toml
[profiles.personal]
provider = "google"
default_tz = "..."
google_oauth_client_file = "~/path/to/google-oauth-desktop-client.json"
tags = ["@personal", "personal", "google", "gmail"]
```

`blumkin auth login` records the signed-in address as `email = "..."` in that
profile the first time (display only - it is never used to pick a profile, and
never rewritten afterwards). `blumkin profiles list` shows it, so two profiles
are tellable apart at a glance; `blumkin doctor` warns if the profile is later
signed in as a different account.

Optional: set `client_id` in toml as well; when omitted it is read from the JSON.
Keep the client JSON mode `0600` and outside the repo.

**Coverage.** Google runs `auth`, all of `calendar` (`update` attaches a Meet
link instead of a Teams link; `create` takes the same `--repeat` recurrence
flags), all of `mail` reads and writes, `people resolve` (own contacts, plus the
Workspace directory on a Workspace account), `chat` `find` / `last` / `send`
/ `edit` / `delete` / `attachments`, and `docs create` (a native Google Doc via
the Docs API; `--folder` targets a folder blumkin itself created).
`meeting get` / `meeting transcription` are
stubbed by design — Google Meet transcript access is deliberately not
implemented ([`docs/DECISIONS.md` D8](./docs/DECISIONS.md)). A few option-level
combinations also still fail closed on a Google profile: `mail list` /
`mail inbox` `--importance` / `--has-attachments` / `--orderby`, and
`calendar suggest --treat-tentative free`. Everything else fails closed with a
clear error.

**Provider differences.**

- `mail folders` lists Gmail labels that act as folders; `mail list --folder`
  still takes the well-known names.
- Mail writes need the `gmail.compose` scope — re-run `blumkin auth login` once
  after upgrading or those calls exit `4` (`missing_scope`).
- Chat `attachments` are listed but Drive-backed files are not downloadable.
- A returned draft `id` is the Gmail draft id; `attachments[].id` is `null`
  (Gmail carries attachments inside the raw message).

**Token file** (written by `blumkin auth login`):
`profiles/<name>/google_token.json`. `graph_timeout_seconds` in toml bounds
Google HTTP and token-refresh calls too. Never commit any of these files.

**Local document reads.** `blumkin docs read --path ./file.pdf --json` reads a
local PDF/DOCX/XLSX/image already on disk (for example from `drive download`,
`drive export`, `mail attachments download`, or `chat attachments download`).
It refuses files larger than 25 MB.
Install only the extras you need:

```bash
uv tool install -e '.[pdf]'
uv tool install -e '.[xlsx]'
```

`--ocr` is only reachable through the PDF code path, so it needs `pdf` too -
`.[ocr]` alone cannot read a PDF:

```bash
uv tool install -e '.[pdf,ocr]'
```

Images (`.png`/`.jpg`/`.jpeg`/`.tif`/`.tiff`/`.bmp`/`.webp`) are always OCR'd -
there is no text layer to fall back to, so `--ocr` is implicit for them and
cannot be passed. They only need the `ocr` extra (not `pdf`), and only the
`tesseract` binary (not `poppler`, which is PDF-only):

```bash
uv tool install -e '.[ocr]'
```

The `ocr` extra also needs system binaries on macOS - `tesseract` for both
PDF and image OCR, `poppler` for PDF page rendering only (image OCR does not
need `poppler`):

```bash
brew install tesseract poppler
```

**Prompt-injection flagging.** `docs read` and `mail get` scan extracted
text/body content for common prompt-injection patterns (instruction-override
phrasing, zero-width/bidi-override characters, link-label/URL mismatches,
role-impersonation blocks, base64 blobs next to "decode/execute" triggers).
Detection is heuristic and advisory only: it never blocks, redacts, or alters
the returned content. A match adds an `injection_warning` field to the JSON
payload (`null` when clean) and a `⚠️ POSSIBLE PROMPT INJECTION DETECTED`
banner to human-formatted output, so the caller can decide whether to act on
the content. See issue #282 for the full design and deferred scope (no
LLM-based detection, no hard-block/confirmation wiring in v1).

## Tests

```bash
uv run pytest -m 'not live'                        # CI-equivalent (mocks / offline)
BLUMKIN_LIVE=1 uv run pytest -m live               # live Graph reads + silent refresh
BLUMKIN_LIVE_GOOGLE=1 uv run pytest -m live_google # live Google reads (Google profile)
```

Live tests need `~/.config/blumkin/` by default (override with `BLUMKIN_CONFIG_DIR`):
`config.toml`, token cache, auth record, and a usable refresh token. The
`live_google` tests instead need a logged-in Google profile (`provider =
"google"`). Never commit those files.

## Security

blumkin acts as **you** over delegated OAuth — no server, no shared secret, no
one else's data. Auth and config live only under `~/.config/blumkin/` and are
never committed.

- **[`docs/SECURITY-AT-A-GLANCE.md`](./docs/SECURITY-AT-A-GLANCE.md)** — one page: what it touches, the auth model, blast radius, how releases are trusted.
- [`SECURITY.md`](./SECURITY.md) — full policy, response targets, and private vulnerability reporting.

## Docs

- [`PLAN.md`](./PLAN.md) — original v1 CLI design (historical; open questions resolved in-place, current surface documented above and in `docs/DECISIONS.md`)  
- [`docs/DECISIONS.md`](./docs/DECISIONS.md) — standing decisions and the design-artifact index  
- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — review model and the static-analysis / scanning toolchain  
- [`HANDOFF.md`](./HANDOFF.md) — open Identity/scope-grant follow-up (WO1162425) referenced from runtime scope errors  
- [`AGENTS.md`](./AGENTS.md) — contributor / agent ground rules  
- [`RETROSPECTIVE-M1.md`](./RETROSPECTIVE-M1.md) — M1 ship retrospective (#11)  
- [`docs/agent-integration.md`](./docs/agent-integration.md) — using blumkin from Cursor / Copilot CLI, and the frozen `skills list --json` contract  
- [`docs/operator-config.md`](./docs/operator-config.md) — optional `~/.config/blumkin/*.md` operator-context files (`email-context.md`)  
- [`docs/RELEASING.md`](./docs/RELEASING.md) — release flow, PyPI trusted publishing, rollback  
- [`docs/google-setup.md`](./docs/google-setup.md) — Google Cloud Desktop OAuth + blumkin Google profile  
- [`.cursor/skills/blumkin/SKILL.md`](./.cursor/skills/blumkin/SKILL.md) — Cursor agent skill  

## License

MIT © 2026 Henrique Andrade ([GitHub's thehcma](https://github.com/thehcma)) — see [`LICENSE`](./LICENSE).
