Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ It gives an agent two capabilities, with safety built into the *structure*, not
`messageRule`s, so deterministic mail organisation lives *in Outlook* (runs even when nothing
else is, visible and editable in Outlook's own UI, reversible by deleting one rule).

## Verbs (skills)

Each skill is a thin wrapper over the stdlib kernel; the runtime catalog is the source of truth
(`python3 -m msgraph.client describe`). Names are prefixed `msgraph-` when invoked as slash commands.

| Skill | Scope it needs | What it does |
|---|---|---|
| `auth-login` | `Mail.Read + MailboxSettings.Read` (read, default) or `+ MailboxSettings.ReadWrite` (`--mode rules`) | Device-code sign-in; caches the token at the XDG path and refreshes it silently. **Run first.** |
| `mail-list` | `Mail.Read` | List recent inbox messages (concise/detailed, pagination default 25). |
| `mail-get` | `Mail.Read` | Fetch one message incl. its internet headers (e.g. `List-Unsubscribe`). |
| `rule-list` | `MailboxSettings.Read` | Enumerate existing inbox rules agent-legibly. |
| `rule-verify` | `Mail.Read` | Compute a candidate rule's **read-only catch-set** and record the verification gate. |
| `rule-create` | `MailboxSettings.ReadWrite` | Install a verified move-to-folder rule; refuses unverified criteria. |
| `rule-remove` | `MailboxSettings.ReadWrite` | Delete a rule by id (the reversibility primitive). |

## Why stdlib / zero-backend

The Microsoft Graph + Outlook plugin space is crowded — but it is almost entirely Node MCP servers
Expand Down Expand Up @@ -47,15 +62,40 @@ CLAUDE.md # grounding + build plan for a Claude C

## Prerequisite (one-time, free)

An **Azure AD app registration** (public client, device-code flow enabled). Add the delegated
permission `Mail.Read` (and `MailboxSettings.ReadWrite` only if you want rule authoring). No cost,
no admin consent for personal accounts. Set the resulting client/tenant IDs via environment before
first auth — see `CLAUDE.md`.
An **Azure AD app registration** (public client, device-code / public-client flow **enabled**). Add
the delegated permissions `Mail.Read` + `MailboxSettings.Read` (read mail and list rules), plus
`MailboxSettings.ReadWrite` only if you want rule authoring. No cost, no admin consent for personal
accounts. Then export the resulting identifiers before first sign-in (read from the environment,
never hardcoded):

```bash
export MSGRAPH_CLIENT_ID="<application (client) id>"
export MSGRAPH_TENANT_ID="consumers" # or "common" for work/school + personal accounts
```

See `plugin/skills/auth-login/SKILL.md` for the full walkthrough.

## Quick start

```bash
python3 -m msgraph.client describe # discover every verb + schema
python3 -m msgraph.client auth-login # read-only sign-in (device code)
python3 -m msgraph.client mail-list --limit 10 # triage the inbox
python3 -m msgraph.client rule-verify --header_contains "List-Unsubscribe" # preview, read-only
python3 -m msgraph.client auth-login --mode rules # escalate (separate consent)
python3 -m msgraph.client rule-create --name "Newsletters" \
--header_contains "List-Unsubscribe" --move_to_folder "Newsletters"
```

(Run from `plugin/src/`, or set `PYTHONPATH=plugin/src`. Inside an installed plugin the skills use
`${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py`.)

## Status

Scaffolded; skills built spec-first (`/speckit-specify` → `clarify` → `plan` → `tasks` →
`implement`). See `DEFINITION_OF_DONE.md` for the target and `CLAUDE.md` for the build plan.
v0.1 implemented spec-first (`/speckit-specify` → `clarify` → `plan` → `tasks` → `implement`): the
seven verbs above, the runtime `describe` catalog, and offline unit tests (Graph HTTP boundary
mocked). Live auth/Graph behaviour requires the one-time Azure app registration above. See
`DEFINITION_OF_DONE.md` for the target and `CLAUDE.md` for the build plan.

## License

Expand Down
18 changes: 0 additions & 18 deletions plugin/hooks/hooks.json

This file was deleted.

76 changes: 76 additions & 0 deletions plugin/skills/auth-login/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
name: "auth-login"
description: "Sign in to Microsoft Graph via the OAuth device-code flow so the other msgraph verbs can reach the mailbox. Run this FIRST, before any mail-* or rule-* command. Default mode is read-only (Mail.Read + MailboxSettings.Read): read mail and list existing rules, with NO capability to change anything. Pass --mode rules only when you need to author rules — it consents to MailboxSettings.ReadWrite as a separate, deliberate escalation. Caches the token outside the repo (0600) and refreshes it silently. Use when a command reports 'not signed in' or 'escalate'. Requires MSGRAPH_CLIENT_ID / MSGRAPH_TENANT_ID in the environment (one-time Azure app registration; see below)."
argument-hint: "[--mode read|rules] (default read)"
user-invocable: true
disable-model-invocation: false
annotations:
readOnlyHint: false # writes the local token cache (not the mailbox)
destructiveHint: false
idempotentHint: false # each login mints a fresh token
openWorldHint: true # talks to the Microsoft identity platform
---

## What this does

Performs the OAuth 2.0 **device authorization grant** by hand (stdlib `urllib` only — no `msal`, no
SDK): it prints a short user code + a `microsoft.com/devicelogin` URL, you authorise in a browser,
and the kernel polls until consent and caches the resulting token at
`${XDG_STATE_HOME:-~/.local/state}/msgraph-stdlib/token.json` (`0600`, outside the repo, never
committed). The refresh token (`offline_access`) is used to renew silently on later calls.

**The two modes are the safety ratchet — choose the smallest that fits the task:**

| Mode | Scopes granted | Lets you | Cannot |
|---|---|---|---|
| `read` (default) | `Mail.Read` + `MailboxSettings.Read` | `mail-list`, `mail-get`, `rule-list`, `rule-verify` | create/remove rules, mutate any mail |
| `rules` | `Mail.Read` + `MailboxSettings.ReadWrite` | the above **plus** `rule-create`, `rule-remove` | mutate individual messages (no verb does) |

A read-only token *structurally* carries no write grant, so even a bug cannot change the mailbox.
Escalating to `--mode rules` is a separate browser consent — the OAuth grant is the audit record.
Stay in `read` until you actually need to install or remove a rule.

## One-time prerequisite (free, human)

Register a free **Azure AD app** (public client, device-code/public-client flow **enabled**) with
delegated permissions `Mail.Read` + `MailboxSettings.Read` (+ `MailboxSettings.ReadWrite` for rule
authoring). Personal Microsoft accounts need no admin consent. Then export, before first sign-in:

```bash
export MSGRAPH_CLIENT_ID="<application (client) id>"
export MSGRAPH_TENANT_ID="consumers" # or "common" for work/school + personal
```

These are read from the environment and never hardcoded. If `MSGRAPH_CLIENT_ID` is unset, the verb
returns a steering error explaining this.

## Discoverability

The kernel's `TOOLS` catalog is the single source of truth for every verb's arguments — don't rely
on this doc staying in sync, ask at runtime:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe # all verbs
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name auth-login # this verb
```

## How it runs

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" auth-login # read-only (default)
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" auth-login --mode rules # escalate to write
# or, as a module: python3 -m msgraph.client auth-login [--mode rules]
```

## Output

The device code + verification URL are printed to stderr; on success a one-line confirmation names
the mode and granted scopes. The access token itself is never printed.

## Errors steer the agent

```
error: MSGRAPH_CLIENT_ID is not set. Register a free Azure AD public client … then export
MSGRAPH_CLIENT_ID and MSGRAPH_TENANT_ID.
error: Device-code sign-in timed out before authorisation. Run /msgraph-auth-login again.
```
85 changes: 0 additions & 85 deletions plugin/skills/example-subject-verb/SKILL.md

This file was deleted.

58 changes: 58 additions & 0 deletions plugin/skills/mail-get/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
name: "mail-get"
description: "Fetch ONE Outlook message by id, including its full internet headers. Read-only (Mail.Read). Use when you need a single message's content or — crucially — its raw headers, e.g. to inspect List-Unsubscribe, Sender, or List-Id before proposing a header-based rule with rule-verify/rule-create. Get the message_id from mail-list --format detailed. concise (default) prints subject/sender/received plus the header list; detailed returns the full JSON. Requires a prior read sign-in (run /msgraph-auth-login)."
argument-hint: "--message_id <id> [--format concise|detailed]"
user-invocable: true
disable-model-invocation: false
annotations:
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
---

## What this does

Fetches a single message via `GET /me/messages/{id}` with `internetMessageHeaders` selected, so you
can read the raw headers Outlook rules match against. Read-only (`Mail.Read`) — reaches no write
endpoint.

This is the natural precursor to **rule-verify**: inspect a representative message's headers here to
choose the right `header_contains` substrings, then verify the catch-set read-only before installing
anything.

## Discoverability

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name mail-get
```

## How it runs

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" mail-get --message_id <id> --format concise
# or: python3 -m msgraph.client mail-get --message_id <id> --format detailed
```

Get `<id>` from `mail-list --format detailed`.

## Output (agent-legible)

```
Subject: Weekly Newsletter
From: news@example.com
Received: 2026-06-19T08:01:00Z

Internet headers (12):
List-Unsubscribe: <mailto:unsubscribe@example.com>
List-Id: Example Newsletter <news.example.com>
...
```

`--format detailed` returns the full message JSON.

## Errors steer the agent

```
error: not signed in — run /msgraph-auth-login first, then retry.
```
55 changes: 55 additions & 0 deletions plugin/skills/mail-list/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
name: "mail-list"
description: "List recent inbox messages from Outlook, agent-legibly, for triage or to find a message to inspect. Read-only (Mail.Read). Use when you need an overview of what's in the inbox — e.g. before proposing a rule, or to locate a message id for mail-get. concise (default) returns readable summaries (subject, sender, received time); detailed adds Graph ids needed for follow-up calls. Bounded by --limit (default 25) so it never dumps the whole mailbox. Requires a prior read sign-in (run /msgraph-auth-login)."
argument-hint: "[--limit N] [--format concise|detailed]"
user-invocable: true
disable-model-invocation: false
annotations:
readOnlyHint: true
destructiveHint: false
idempotentHint: true
openWorldHint: true
---

## What this does

Fetches recent inbox messages via `GET /me/messages` (newest first), resolving the sender to a
readable address and shaping the output for an agent. It reads only — it holds `Mail.Read` and
reaches no write endpoint, so it cannot move, archive, or delete anything.

Reach for this for **triage/overview**. When you need one message's full content or its internet
headers (e.g. to inspect `List-Unsubscribe` before proposing a rule), follow up with **mail-get**
using an id from `--format detailed`.

## Discoverability

The kernel's `TOOLS` catalog is the source of truth for arguments — ask at runtime rather than
trusting this doc:

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name mail-list
```

## How it runs

```bash
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" mail-list --limit 10 --format concise
# or: python3 -m msgraph.client mail-list ...
```

## Output (agent-legible)

`concise` returns readable summaries; pass `--format detailed` only when you need ids for a
follow-up call (`mail-get`):

```
- "Weekly Newsletter" from news@example.com (received: 2026-06-19T08:01:00Z)
- "Receipt #4471" from billing@example.com (received: 2026-06-18T22:14:00Z)
2 message(s). Pass --format detailed for IDs needed by follow-up commands.
```

## Errors steer the agent

```
error: not signed in — run /msgraph-auth-login first, then retry.
```
Loading
Loading