Skip to content

Commit 6dea251

Browse files
authored
Merge pull request #5 from neilgfoster/003-categorise-and-search-folders
feat: category-assigning rules + category & search-folder verbs
2 parents a8ac008 + 331eb4d commit 6dea251

12 files changed

Lines changed: 1036 additions & 55 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,11 @@ or advertise it.
101101
reference bundled files via `${CLAUDE_PLUGIN_ROOT}/...` — it resolves to `plugin/`.
102102

103103
<!-- SPECKIT START -->
104-
Active feature plan: `specs/002-fix-graph-query-encoding/plan.md` (defect fix: percent-encode all
105-
Graph query params + real-URL regression coverage + Azure setup docs). Foundation feature plan:
106-
`specs/001-msgraph-mail-rules/plan.md` (spec, research, data-model, contracts/tools.md, quickstart
107-
alongside it) — read it for the technical context, the stdlib-only device-code design, the `TOOLS`
108-
catalog contract, and the safety-model decisions before implementing.
104+
Active feature plan: `specs/003-categorise-and-search-folders/plan.md` (extend the kernel with
105+
category-assigning rules + category-ensure under the existing rule-authoring scope, and
106+
search-folder create/list/remove behind a NEW `Mail.ReadWrite` auth tier `--mode folders`; research,
107+
data-model, contracts/tools.md, quickstart alongside it). Prior feature plans:
108+
`specs/002-fix-graph-query-encoding/plan.md` (percent-encode Graph query params + real-URL coverage)
109+
and `specs/001-msgraph-mail-rules/plan.md` (foundation: stdlib-only device-code design, `TOOLS`
110+
catalog contract, safety-model decisions) — read them for the technical context before implementing.
109111
<!-- SPECKIT END -->

DEFINITION_OF_DONE.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@ plugin is "done" for its first release when **every** box below is true. Read `C
77

88
- [ ] **Auth** — device-code login works end-to-end; token cached at
99
`${XDG_STATE_HOME:-~/.local/state}/msgraph-stdlib/token.json` (`0600`, outside the repo) and
10-
transparently refreshed. Two distinct modes: read-only (`Mail.Read`) and
11-
rule-authoring (`+ MailboxSettings.ReadWrite`).
10+
transparently refreshed. Three distinct modes (the scope ratchet): read-only (`Mail.Read`),
11+
rule-authoring (`+ MailboxSettings.ReadWrite`), and search-folder (`Mail.ReadWrite`).
1212
- [ ] **Mail read**`mail-list` and `mail-get` return messages and their internet headers,
1313
read-only, with concise/detailed output and a sane pagination default.
1414
- [ ] **Rule read**`rule-list` enumerates existing Outlook `messageRule`s, agent-legibly.
1515
- [ ] **Rule verify**`rule-verify` takes candidate predicates and returns the **read-only
1616
catch-set** (the messages the rule would match) without writing anything.
17-
- [ ] **Rule create**`rule-create` installs a predicate→move-to-folder rule, and **refuses**
18-
unless a catch-set has been verified for it.
17+
- [ ] **Rule create**`rule-create` installs a predicate→move-to-folder **and/or assign-category**
18+
rule, and **refuses** unless a catch-set has been verified for it (and unless it has an action).
1919
- [ ] **Rule remove**`rule-remove` deletes a rule by id (the reversibility primitive).
20+
- [ ] **Categories**`category-list` enumerates the mailbox master categories; `category-ensure`
21+
create-if-absent a named category (coloured), under the rule-authoring scope. `rule-create`
22+
ensures any category it assigns so labels always render with a colour.
23+
- [ ] **Search folders**`searchfolder-create` makes a virtual `mailSearchFolder` (a saved,
24+
category-filtered view; never moves/deletes mail) under the **separate** `Mail.ReadWrite` tier;
25+
`searchfolder-list` enumerates them agent-legibly; `searchfolder-remove` deletes one by id
26+
(reversibility primitive — affects only the virtual folder, never mail).
2027

2128
## Constraints (any failure = not done)
2229

@@ -26,10 +33,13 @@ plugin is "done" for its first release when **every** box below is true. Read `C
2633
the working tree are clean of secrets; storage is the external XDG path only. No git-crypt
2734
dependency.
2835
- [ ] **Read cannot mutate.** Read-only mode holds `Mail.Read` only; no write endpoint is reachable
29-
from any read skill. Write capability exists *only* after explicit `MailboxSettings.ReadWrite`
30-
opt-in.
36+
from any read skill. Write capability exists *only* after explicit opt-in, and each write tier is
37+
a **separate, distinctly-consented** scope: rule/category authoring (`MailboxSettings.ReadWrite`)
38+
and search-folder creation (`Mail.ReadWrite`, `--mode folders`) are independent ratchet steps —
39+
neither read nor rule-authoring tokens can create a search folder.
3140
- [ ] **No imperative per-message mutation.** The plugin never archives/moves/deletes an individual
32-
message itself; organisation happens only via installed rules, which file (never delete).
41+
message itself; organisation happens only via installed rules, which file or label (never delete).
42+
Search folders are **virtual saved views** — creating or removing one moves/deletes no mail.
3343
- [ ] **Agent-friendly.** Every skill meets `docs/AGENT-FRIENDLY.md`: onboarding-quality
3444
`description` (incl. when-to-use), flat JSON-schema inputs, agent-legible output (IDs resolved
3545
to names; concise/detailed), steering error messages, accurate `annotations`.

README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
A Claude Code plugin for Microsoft Graph (Outlook) that is **stdlib-only, zero-dependency, and
44
zero-backend** by design — just `urllib` + `json`, no SDK, no server process, no install friction.
55

6-
It gives an agent two capabilities, with safety built into the *structure*, not the behaviour:
6+
It gives an agent these capabilities, with safety built into the *structure*, not the behaviour:
77

88
1. **Read Outlook mail** — list/get messages and their headers, read-only.
99
2. **Author native Outlook message rules** — create / list / verify / remove server-side
1010
`messageRule`s, so deterministic mail organisation lives *in Outlook* (runs even when nothing
11-
else is, visible and editable in Outlook's own UI, reversible by deleting one rule).
11+
else is, visible and editable in Outlook's own UI, reversible by deleting one rule). A rule can
12+
file to a folder **and/or assign a coloured category**.
13+
3. **Manage master categories** — list and create-if-absent the coloured labels a rule assigns, so
14+
they always render with a colour.
15+
4. **Create category search folders** — virtual `mailSearchFolder` views over a category, behind a
16+
*separate* `Mail.ReadWrite` sign-in tier. A search folder never moves or deletes mail.
1217

1318
## Verbs (skills)
1419

@@ -17,13 +22,18 @@ Each skill is a thin wrapper over the stdlib kernel; the runtime catalog is the
1722

1823
| Skill | Scope it needs | What it does |
1924
|---|---|---|
20-
| `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.** |
25+
| `auth-login` | `Mail.Read + MailboxSettings.Read` (read, default), `+ MailboxSettings.ReadWrite` (`--mode rules`), or `Mail.ReadWrite` (`--mode folders`) | Device-code sign-in; caches the token at the XDG path and refreshes it silently. **Run first.** |
2126
| `mail-list` | `Mail.Read` | List recent inbox messages (concise/detailed, pagination default 25). |
2227
| `mail-get` | `Mail.Read` | Fetch one message incl. its internet headers (e.g. `List-Unsubscribe`). |
2328
| `rule-list` | `MailboxSettings.Read` | Enumerate existing inbox rules agent-legibly. |
2429
| `rule-verify` | `Mail.Read` | Compute a candidate rule's **read-only catch-set** and record the verification gate. |
25-
| `rule-create` | `MailboxSettings.ReadWrite` | Install a verified move-to-folder rule; refuses unverified criteria. |
30+
| `rule-create` | `MailboxSettings.ReadWrite` | Install a verified rule that files to a folder **and/or assigns a category**; refuses unverified criteria. |
2631
| `rule-remove` | `MailboxSettings.ReadWrite` | Delete a rule by id (the reversibility primitive). |
32+
| `category-list` | `MailboxSettings.Read` | List the mailbox master categories (name + colour). |
33+
| `category-ensure` | `MailboxSettings.ReadWrite` | Create a coloured category if absent (idempotent). |
34+
| `searchfolder-list` | `Mail.Read` | Enumerate existing virtual search folders agent-legibly. |
35+
| `searchfolder-create` | `Mail.ReadWrite` (`--mode folders`) | Create a virtual category search-folder view; never touches mail. |
36+
| `searchfolder-remove` | `Mail.ReadWrite` (`--mode folders`) | Delete a search folder by id (affects only the view, never mail). |
2737

2838
## Why stdlib / zero-backend
2939

@@ -36,14 +46,16 @@ with no supply-chain surface beyond the standard library.
3646

3747
- **Read-only by default.** Auth requests **`Mail.Read` only**. The plugin physically cannot move,
3848
archive, or delete a message — the token carries no write grant. Safety is structural.
39-
- **Scope ratchet.** Writing rules requires the *separate* `MailboxSettings.ReadWrite` scope,
40-
granted only when you opt into rule authoring. Escalation is deliberate and auditable (the OAuth
41-
consent is the record).
49+
- **Scope ratchet.** Writing rules/categories requires the *separate* `MailboxSettings.ReadWrite`
50+
scope, and creating search folders requires a *further separate* `Mail.ReadWrite` tier
51+
(`--mode folders`) — each granted only when you opt in. Escalation is deliberate and auditable (the
52+
OAuth consent is the record); a read or rule-authoring token cannot create a search folder.
4253
- **Verify before install.** A candidate rule's **real catch-set is computed read-only** and shown
4354
*before* the rule is created — a rule is never trusted in the abstract (Graph `headerContains` is
4455
coarse substring matching, so it must be checked against actual mail).
45-
- **Reversible by construction.** Rules file mail to a folder; they never delete. Removing one rule
46-
undoes the organisation.
56+
- **Reversible by construction.** Rules file or label mail; they never delete. Removing one rule
57+
undoes the organisation. Search folders are virtual saved views — creating or removing one never
58+
moves or deletes a message.
4759

4860
## Layout
4961

@@ -64,7 +76,8 @@ CLAUDE.md # grounding + build plan for a Claude C
6476

6577
An **Azure AD app registration** (public client, device-code / public-client flow **enabled**). Add
6678
the delegated permissions `Mail.Read` + `MailboxSettings.Read` (read mail and list rules), plus
67-
`MailboxSettings.ReadWrite` only if you want rule authoring. No cost, no admin consent for personal
79+
`MailboxSettings.ReadWrite` only if you want rule/category authoring, plus `Mail.ReadWrite` only if
80+
you want to create search folders (`--mode folders`). No cost, no admin consent for personal
6881
accounts. Then export the resulting identifiers before first sign-in (read from the environment,
6982
never hardcoded):
7083

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
name: "category-ensure"
3+
description: "Ensure a named Outlook master category exists: create it with a colour if absent (POST /me/outlook/masterCategories), no-op if it already exists. Idempotent. Requires rule-authoring sign-in (/msgraph-auth-login --mode rules; MailboxSettings.ReadWrite). Use to pre-create a label with a specific colour before assigning it; rule-create already ensures any category it assigns, so calling this first is optional unless you want to choose the colour. A category's display name is immutable once created. Pass --name and an optional --color preset (default preset9)."
4+
argument-hint: "--name <category name> [--color presetN]"
5+
user-invocable: true
6+
disable-model-invocation: false
7+
annotations:
8+
readOnlyHint: false
9+
destructiveHint: false
10+
idempotentHint: true
11+
openWorldHint: true
12+
---
13+
14+
## What this does
15+
16+
Create-if-absent for a master category. Lists the master categories, matches `--name`
17+
case-insensitively, and if missing creates it (`POST /me/outlook/masterCategories` with
18+
`{displayName, color}`). If present, it reports a no-op. Needs `MailboxSettings.ReadWrite` — the same
19+
rule-authoring tier; no new scope.
20+
21+
Use it when you want a label to render with a **specific colour**. `rule-create --assign_category`
22+
ensures categories automatically (defaulting the colour to `preset9`), so this verb is only needed
23+
for colour control or to seed labels ahead of time. The display name cannot be changed after creation.
24+
25+
## Discoverability
26+
27+
```bash
28+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name category-ensure
29+
```
30+
31+
## How it runs
32+
33+
```bash
34+
/msgraph-auth-login --mode rules
35+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" category-ensure --name "Needs attention" --color preset9
36+
# or: python3 -m msgraph.client category-ensure --name "Receipts" --color preset5
37+
```
38+
39+
## Output
40+
41+
```
42+
Created category "Needs attention" (preset9). It now renders with a colour.
43+
Category "Receipts" already exists (preset5); no change.
44+
```
45+
46+
## Errors steer the agent
47+
48+
```
49+
error: This action needs rule-authoring permission … run /msgraph-auth-login --mode rules.
50+
```
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
name: "category-list"
3+
description: "List the mailbox's Outlook master categories (each label's name and colour), read-only, via GET /me/outlook/masterCategories. Requires read sign-in (/msgraph-auth-login; MailboxSettings.Read). Use to see which labels already exist before authoring an assign-category rule (rule-create --assign_category) or a category search folder (searchfolder-create --category). Pairs with category-ensure, which create-if-absent a named category. concise (default) shows name + colour; detailed returns full JSON."
4+
argument-hint: "[--format concise|detailed]"
5+
user-invocable: true
6+
disable-model-invocation: false
7+
annotations:
8+
readOnlyHint: true
9+
destructiveHint: false
10+
idempotentHint: true
11+
openWorldHint: true
12+
---
13+
14+
## What this does
15+
16+
Lists the mailbox master category list (`GET /me/outlook/masterCategories`) — the named, coloured
17+
labels Outlook uses. Read-only; needs only `MailboxSettings.Read` (the default read sign-in). Use it
18+
to discover existing labels before you assign one in a rule or filter on one in a search folder.
19+
20+
## Discoverability
21+
22+
```bash
23+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name category-list
24+
```
25+
26+
## How it runs
27+
28+
```bash
29+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" category-list
30+
# or: python3 -m msgraph.client category-list --format detailed
31+
```
32+
33+
## Output
34+
35+
```
36+
- "Needs attention" (preset9)
37+
- "Receipts" (preset5)
38+
2 categor(y/ies).
39+
```
40+
41+
## Errors steer the agent
42+
43+
```
44+
error: Not signed in — run /msgraph-auth-login first, then retry.
45+
```

plugin/skills/rule-create/SKILL.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
name: "rule-create"
3-
description: "Install a native Outlook message rule that FILES matching mail to a folder (move-to-folder only — never delete). Requires rule-authoring sign-in (run /msgraph-auth-login --mode rules; MailboxSettings.ReadWrite). REFUSES unless the exact same header_contains criteria were verified first with rule-verify — verify-then-install is a hard safety gate, not a convention. Use after you have inspected headers (mail-get) and confirmed the catch-set (rule-verify). The rule appears in Outlook's own Rules UI and is fully reversible: remove it with rule-remove and any mail already filed stays put. Pass a name, the verified header_contains substrings, and an existing target folder name."
4-
argument-hint: "--name <rule name> --header_contains SUBSTR [SUBSTR ...] --move_to_folder <folder name>"
3+
description: "Install a native Outlook message rule that FILES matching mail to a folder and/or ASSIGNS a category to it (move-to-folder and/or assign-category — never delete). Requires rule-authoring sign-in (run /msgraph-auth-login --mode rules; MailboxSettings.ReadWrite). REFUSES unless the exact same header_contains criteria were verified first with rule-verify — verify-then-install is a hard safety gate, not a convention — and refuses if you give no action at all. Any assigned category is ensured to exist (coloured) first. Use after you have inspected headers (mail-get) and confirmed the catch-set (rule-verify). The rule appears in Outlook's own Rules UI and is fully reversible: remove it with rule-remove and any mail already filed/labelled stays put. Pass a name, the verified header_contains substrings, and at least one of --move_to_folder or --assign_category."
4+
argument-hint: "--name <rule name> --header_contains SUBSTR [SUBSTR ...] [--move_to_folder <folder name>] [--assign_category NAME ...]"
55
user-invocable: true
66
disable-model-invocation: false
77
annotations:
@@ -14,9 +14,17 @@ annotations:
1414
## What this does
1515

1616
Creates a server-side `messageRule` under `POST /me/mailFolders/inbox/messageRules` with a
17-
`headerContains` predicate and a **`moveToFolder` action only**. No delete-style action is ever
18-
constructed, so installing a rule only ever *files* mail — removing the rule undoes the organisation
19-
and mail already filed stays put.
17+
`headerContains` predicate and a **`moveToFolder` and/or `assignCategories` action** — you must supply
18+
at least one (no action → refusal). No delete-style action is ever constructed, so installing a rule
19+
only ever *files* and/or *labels* mail — removing the rule undoes the organisation and mail already
20+
filed/labelled stays put.
21+
22+
**Assign-category action.** Pass `--assign_category "Needs attention"` (repeatable) to tag matching
23+
mail with one or more Outlook categories — alone, or combined with `--move_to_folder` to label **and**
24+
file. Before installing, each named category is **ensured to exist** in the master list (created with
25+
a colour if absent, via the same `MailboxSettings.ReadWrite` scope), so the label always renders
26+
meaningfully. Use `category-list` to see existing labels and `category-ensure` to pre-create one with
27+
a specific colour.
2028

2129
**Two gates must pass, or it refuses (this is the heart of the safety model):**
2230

@@ -37,6 +45,9 @@ mail to a folder, they never create or delete one.
3745
/msgraph-auth-login --mode rules # escalate (separate consent)
3846
/msgraph-rule-create --name "Newsletters" \
3947
--header_contains "List-Unsubscribe" --move_to_folder "Newsletters"
48+
# or label on arrival (alone or combined with --move_to_folder):
49+
/msgraph-rule-create --name "Flag newsletters" \
50+
--header_contains "List-Unsubscribe" --assign_category "Needs attention"
4051
```
4152

4253
## Discoverability

0 commit comments

Comments
 (0)