ask-marcel-office-cli 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/README.md +178 -119
  3. package/dist/cli.js +44806 -27142
  4. package/dist/commands.json +371 -52
  5. package/dist/composition/build-deps.d.ts +8 -0
  6. package/dist/composition/mcp.d.ts +20 -0
  7. package/dist/composition/run-registry-command.d.ts +34 -0
  8. package/dist/domain/tenant-id.d.ts +9 -0
  9. package/dist/domain/utilities/spo-tenant.d.ts +20 -0
  10. package/dist/index.js +2735 -1674
  11. package/dist/infra/auth.d.ts +36 -14
  12. package/dist/infra/browser-auth.d.ts +7 -2
  13. package/dist/infra/graph-client.d.ts +52 -16
  14. package/dist/presenter/graph-cursor.d.ts +2 -0
  15. package/dist/presenter/output.d.ts +1 -1
  16. package/dist/presenter/render-to-string.d.ts +15 -0
  17. package/dist/use-cases/commands/build-command.d.ts +19 -2
  18. package/dist/use-cases/commands/command-types.d.ts +16 -11
  19. package/dist/use-cases/commands/convert-calendar-event-attachment-to-markdown.d.ts +4 -0
  20. package/dist/use-cases/commands/{convert-drive-item-zip.d.ts → convert-drive-item-zip-to-markdown.d.ts} +5 -0
  21. package/dist/use-cases/commands/{convert-local-file.d.ts → convert-local-file-to-markdown.d.ts} +4 -0
  22. package/dist/use-cases/commands/convert-mail-attachment-to-markdown.d.ts +10 -2
  23. package/dist/use-cases/commands/{convert-mail-attachment-zip.d.ts → convert-mail-attachment-zip-to-markdown.d.ts} +5 -1
  24. package/dist/use-cases/commands/convert-mail-to-markdown.d.ts +35 -1
  25. package/dist/use-cases/commands/create-forward-draft.d.ts +4 -0
  26. package/dist/use-cases/commands/create-reply-draft.d.ts +8 -0
  27. package/dist/use-cases/commands/download-drive-item-as-markdown.d.ts +7 -2
  28. package/dist/use-cases/commands/download-drive-item-as-pdf.d.ts +1 -0
  29. package/dist/use-cases/commands/download-drive-item-content.d.ts +1 -0
  30. package/dist/use-cases/commands/draft-comment-splicer.d.ts +29 -0
  31. package/dist/use-cases/commands/draft-dedup.d.ts +18 -0
  32. package/dist/use-cases/commands/draft-response.d.ts +4 -0
  33. package/dist/use-cases/commands/extract-drive-item-images.d.ts +1 -0
  34. package/dist/use-cases/commands/extract-local-file-images.d.ts +1 -1
  35. package/dist/use-cases/commands/fetch-raw-bytes.d.ts +13 -0
  36. package/dist/use-cases/commands/find-mail-drafts.d.ts +9 -0
  37. package/dist/use-cases/commands/get-mail-signature.d.ts +8 -0
  38. package/dist/use-cases/commands/get-user.d.ts +10 -0
  39. package/dist/use-cases/commands/inline-image-embedder.d.ts +2 -1
  40. package/dist/use-cases/commands/login-status.d.ts +9 -27
  41. package/dist/use-cases/commands/login.d.ts +21 -0
  42. package/dist/use-cases/commands/mail-message-select.d.ts +1 -0
  43. package/dist/use-cases/commands/mail-quote-stripper.d.ts +16 -15
  44. package/dist/use-cases/commands/markdown-dispatch.d.ts +2 -1
  45. package/dist/use-cases/commands/msg-to-markdown.d.ts +10 -1
  46. package/dist/use-cases/commands/odata-query.d.ts +15 -1
  47. package/dist/use-cases/commands/office-to-markdown.d.ts +1 -0
  48. package/dist/use-cases/commands/read-mail-attachment.d.ts +4 -0
  49. package/dist/use-cases/commands/resolve-command.d.ts +25 -0
  50. package/dist/use-cases/commands/search-all-files.d.ts +8 -0
  51. package/dist/use-cases/commands/search-escape.d.ts +23 -0
  52. package/dist/use-cases/commands/signature-extractor.d.ts +2 -0
  53. package/dist/use-cases/commands/tenant-option.d.ts +44 -0
  54. package/dist/use-cases/commands/update-mail-draft.d.ts +1 -0
  55. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +10 -5
  56. package/dist/use-cases/ports/filesystem.d.ts +1 -1
  57. package/docs/COMMANDS.md +34 -29
  58. package/docs/USAGE.md +65 -7
  59. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,209 @@
2
2
 
3
3
  All notable changes to `ask-marcel-office-cli` are documented here.
4
4
 
5
+ ## 2.2.0
6
+
7
+ ### Added
8
+
9
+ - **`ask-marcel-office mcp` — serve the whole CLI to any MCP client over stdio.** Hosts
10
+ without a shell (Claude Desktop, other MCP clients) can now reach every
11
+ command. Register it with:
12
+
13
+ ```bash
14
+ claude mcp add --transport stdio --scope user ask-marcel-office -- ask-marcel-office mcp
15
+ ```
16
+
17
+ It exposes **five gateway tools**, not one per command — 183 tool schemas
18
+ would inject hundreds of KB into every session, the exact token bloat this
19
+ CLI exists to avoid:
20
+
21
+ - `list-commands` — the terse manifest (`{name, summary, category}`),
22
+ optionally filtered to one category. The discovery entry point.
23
+ - `get-command-docs` — full Markdown docs for a single command: every option,
24
+ its Graph endpoint, an example, the response shape. Lifecycle commands and
25
+ deprecated command names both resolve.
26
+ - `run-command` — runs any of the **179 read** commands. Declared
27
+ `readOnlyHint: true`, so an MCP client can auto-approve it; a write routed
28
+ here is refused *before* it executes.
29
+ - `run-write-command` — runs the **4** mail-draft write commands
30
+ (`create-mail-draft`, `create-forward-draft`, `create-reply-draft`,
31
+ `update-mail-draft`). A separate tool precisely so the 179 read commands
32
+ keep an honest read-only annotation. Marked non-destructive: every write
33
+ produces an UNSENT draft, and this CLI still cannot send mail.
34
+ - `login` — sign in or refresh. The elevated (M365) token lapses roughly
35
+ hourly and only a browser can recapture it, so this saves a terminal
36
+ round-trip mid-session.
37
+
38
+ Both run tools accept `outputPath` / `outputDir`, mirroring the CLI flags, so
39
+ a multi-MB PDF lands on disk instead of flooding the model's context.
40
+
41
+ **Raise your MCP client's tool timeout to ~5 minutes** (`MCP_TOOL_TIMEOUT=300000`
42
+ or equivalent). Measured against a real tenant: a browser sign-in takes
43
+ **37–64 s even with no MFA prompt**, while the MCP default request timeout is
44
+ **60 s** — so `login` times out intermittently at the default. The server keeps
45
+ running through a client-side timeout, so the sign-in has usually completed
46
+ anyway; re-run your original command before calling `login` again. Sign in
47
+ from a terminal the first time (`ask-marcel-office login`), where an MFA prompt adds
48
+ minutes on top.
49
+
50
+ Read/write sets are derived from the registry's `mutates` flag, so a new
51
+ command lands on the correct tool with no code change here.
52
+
53
+ - **Files in a partner tenant you are a guest in are now readable.** Previously
54
+ every such read died at `401 invalidAudienceUri: Invalid audience Uri
55
+ '00000003-0000-0ff1-ce00-000000000000'` (SharePoint Online's app id): your
56
+ home-tenant Graph cannot mint a SharePoint token for a foreign tenant, so no
57
+ home-tier token could reach the file — which is exactly the case when a partner
58
+ sends you a "Copy link" URL.
59
+
60
+ **`resolve-drive-share-link` crosses the boundary by itself.** It tries your
61
+ home token and, only on that specific error, identifies the owning tenant from
62
+ the URL host (`contoso.sharepoint.com` → `contoso.onmicrosoft.com` → the
63
+ tenant's public OIDC discovery document) and retries with a guest token minted
64
+ by redeeming your existing refresh token against that tenant's authority. No
65
+ new sign-in, no browser, no Azure app. It returns the tenant as **`tenantId`**;
66
+ the field's PRESENCE is the signal — absent means the file is in your own
67
+ tenant and nothing changes.
68
+
69
+ **`--tenant-id` carries it to the rest of the family.** `driveId` and `itemId`
70
+ carry no tenant, so the commands that consume them cannot recover on their own;
71
+ pass the `tenantId` that `resolve-drive-share-link` returned and they sign with
72
+ the guest token instead. Available on `get-drive-item`,
73
+ `download-drive-item-content`, `download-drive-item-as-markdown`,
74
+ `download-drive-item-as-pdf`, `extract-drive-item-images`,
75
+ `convert-drive-item-zip`, `get-drive-item-list-item`, `list-folder-files`,
76
+ `list-drive-item-versions`, `list-drive-item-permissions`, and
77
+ `list-drive-item-thumbnails`. Optional everywhere; omit it for your own tenant.
78
+
79
+ Boundaries worth knowing: a link you simply may not read still fails with
80
+ `accessDenied` (a guest token would not help, so one is never requested); a
81
+ tenant that has not consented to this client, or where you are not actually a
82
+ guest, fails with a message naming that tenant; a SharePoint host whose sign-in
83
+ domain differs from its name cannot be resolved and says so (`--tenant-id`
84
+ passed by hand still works there); and `1drv.ms` belongs to no tenant at all,
85
+ so it is unaffected. Elevated commands do not take the flag — the elevated
86
+ token is a home-tenant identity, so "elevated in a partner tenant" does not
87
+ exist. Verified end to end against a real partner tenant.
88
+
89
+ - **`get-user`** looks up a directory user by id, UPN/email, or name — one
90
+ command, two routes. An **Azure AD id, UPN, or email** returns that user's FULL
91
+ profile via `GET /users/{id}` on the elevated M365 token (`User.Read.All`, so
92
+ `jobTitle` / `department` / `officeLocation` / phones are populated); it honours
93
+ `--select` / `--expand` and fail-fasts with `secondary_token_unavailable` when
94
+ the elevated token is cold (run `login --force`). An email that is the user's
95
+ `mail` but not their sign-in UPN — every **guest / B2B** user, whose UPN is the
96
+ `alias_homeorg#EXT#@tenant` form — still resolves: when the direct
97
+ `GET /users/{id}` 404s, the command falls back to
98
+ `GET /users?$filter=mail eq '<email>'` and returns the single match. A bare **name** instead
99
+ searches the signed-in user's relevant-people graph
100
+ (`GET /me/people?$search="name"`) on the basic token — so it works even when the
101
+ elevated token is cold — and returns candidate matches
102
+ (`{ id, displayName, mail, jobTitle, department }`) so the caller can
103
+ disambiguate and re-query by the chosen `id` for the full card. Name search
104
+ covers your people graph, not the whole tenant; `microsoft-search-query` remains
105
+ the broad tenant-wide person search. (181st command.)
106
+
107
+ ### Changed
108
+
109
+ - **Command execution is now shared by both front ends.** The per-command
110
+ handler (alias normalization, local-filesystem routing, error-source
111
+ classification, `--output-path` / `--output-dir` persistence) moved out of
112
+ `cli.ts` into `composition/run-registry-command.ts`, which the CLI and the MCP
113
+ gateway both call. Behaviour is unchanged — the existing CLI suite passes
114
+ untouched — but a future fix now reaches both surfaces instead of one.
115
+ - **The output envelope logic is now pure and reusable.** `presenter/output.ts`
116
+ wrote directly to stdout, which an MCP stdio server cannot do (stdout is its
117
+ JSON-RPC channel). The envelope layer moved to `presenter/render-to-string.ts`
118
+ (`renderToString` / `renderErrorToString`); `output.ts` is a thin stdout shim
119
+ over it. Output is byte-identical. MCP callers therefore get the same
120
+ `hint:` / `source:` remedies the terminal does.
121
+
122
+ - **`scopes-check` is now the single detailed token-status view.** Per token
123
+ (basic / elevated / chatsvcagg / ic3) it reports the `available` flag,
124
+ seconds-to-expiry, `refresh` route (`automatic` = self-heals from the shared
125
+ refresh token; `interactive` = the elevated token, needs a browser login), and
126
+ — new — that token's OWN granted scopes, decoded from its `scp` claim. The four
127
+ tokens carry distinct scope sets (basic ~31 Graph scopes, elevated ~20,
128
+ chatsvcagg `user_impersonation`, ic3 `Teams.AccessAsUser.All`), so an agent can
129
+ intersect each tier against a command's `scopesRequired`. Additive and
130
+ non-breaking: the flat top-level `scopes`/`audience`/`expiresAt`/`expiresInSeconds`
131
+ (the basic token) are unchanged. A `hint` field names a forced re-login as the
132
+ single refresh action.
133
+ - **`login` slimmed to an auth confirmation.** It now prints
134
+ `{ status: "authenticated", available: [...], hint }` — which token tiers are
135
+ available, plus a pointer to `scopes-check` (per-token detail) and `login --force`
136
+ (refresh) — instead of the four-token detail block from 2.1.0, and the confusing
137
+ refresh-mechanism hint is gone. The detailed per-token status now lives only in
138
+ `scopes-check`. (Dropping login's `tokens` block is the one breaking-ish change;
139
+ it shipped a single release earlier and the same detail is fully available via
140
+ `scopes-check`.)
141
+ - **The mail read commands now include `conversationId` in their default
142
+ projection.** `list-mail-messages`, `search-mail-messages`, and
143
+ `get-mail-message` add `conversationId` to their slim default `--select`, so a
144
+ caller can group results into a thread — or hand the id straight to
145
+ `list-conversation-messages` — without a second round-trip. The three
146
+ previously-duplicated default-select strings are now one shared constant
147
+ (`mail-message-select.ts`) so they cannot drift apart again. Additive
148
+ (~76 bytes/message); a user-supplied `--select` still overrides entirely.
149
+ - **`resolve-drive-share-link` now resolves a sharing URL to the driveItem in one
150
+ call.** It previously only encoded the URL into the `u!` share token (offline,
151
+ no Graph call), forcing a second `/shares/{token}/driveItem` fetch plus
152
+ hand-parsing `parentReference.driveId`. It now encodes AND fetches, returning
153
+ `{ driveId, itemId, name, webUrl, size, lastModifiedDateTime, shareToken }` — the
154
+ two ids every `*-drive-item` command needs, in one shot (basic token,
155
+ `Files.Read.All`). Shape + behavior change: it now makes a Graph call and no
156
+ longer returns `graphPath`/`originalUrl`; a cross-tenant or no-access link
157
+ surfaces the Graph `accessDenied` / `itemNotFound` instead of a share token that
158
+ would only fail on the follow-up call.
159
+
160
+ ### Fixed
161
+
162
+ - **`login` no longer sends you round a loop it cannot break.** A command needing
163
+ the elevated (M365) token failed with "run `ask-marcel-office login`"; `login`
164
+ answered `authenticated` and changed nothing; the command failed identically.
165
+ Forever. The elevated token carries no refresh token of its own, so a plain
166
+ `login` that found a valid cached basic token returned on the cache rung
167
+ without ever re-capturing it — `--force` was the only escape, and the error
168
+ message never said so.
169
+
170
+ `login` now re-captures the elevated token whenever it is missing, and does
171
+ nothing when it is already present (no gratuitous browser). The fail-fast
172
+ message now names the right remedy per tier and explains why: `--force` for
173
+ elevated, plain `login` for the chatsvcagg / ic3 substrate tokens, which
174
+ self-heal from the shared refresh token and never needed a browser at all.
175
+ `scopes-check`'s hint and the README carried the same false "only way" claim and
176
+ are corrected too. `--force` still works and still re-captures every tier.
177
+
178
+ - **The library's documented `AuthManager` example did not compile.** The README's
179
+ "bring your own token" snippet passed `{getAccessToken, logout}` to
180
+ `createGraphClient` and called `AuthManager` "two async methods"; it has ten.
181
+ The snippet now compiles, and declines the tiers a custom token source cannot
182
+ mint rather than omitting them.
183
+
184
+ - **Guest / external-user (B2B) lookups by UPN now resolve.** A guest UPN is
185
+ `alice_contoso.com#EXT#@fabrikam.onmicrosoft.com`, and the `#` is the URL
186
+ fragment delimiter — the seven commands that put a caller-supplied `userId`
187
+ in a `/users/{id}` path (`get-user-manager`, `get-shared-mailbox-message`,
188
+ `list-shared-mailbox-messages`, `list-shared-mailbox-folder-messages`,
189
+ `list-shared-calendar-events`, `list-user-direct-reports`,
190
+ `list-shared-calendar-view`) interpolated it raw, so `fetch` dropped everything
191
+ from the `#` onward and queried the wrong user. The `userId` segment is now
192
+ percent-encoded (`#`→`%23`, `@`→`%40`; GUIDs unchanged), which Microsoft Graph
193
+ requires for B2B UPNs and which is verified live against the directory. `get-user`
194
+ (new this release) already encodes its path.
195
+ - **Cached elevated / substrate tokens now survive a basic-token refresh.** The
196
+ silent basic-token refresh (triggered whenever the Teams token nears expiry)
197
+ rebuilt the cache from only the three basic fields, wiping the cached `elevated`
198
+ / `chatsvcagg` / `ic3` tokens on every renewal — so `scopes-check` and `login`
199
+ then reported them unavailable with empty scopes, and the elevated token (which
200
+ carries no refresh token of its own) needed a forced re-login to recover. The
201
+ refresh now merges, preserving all four tokens until they each expire.
202
+ - **`scopes-check` explains an unavailable token.** Every tier block whose
203
+ `available` is `false` now carries a `reason` — a one-line note on why it is
204
+ absent and how to restore it (`login --force`; the substrate tiers also self-heal
205
+ on next use) — so an empty `scopes: []` on a missing token is not misread as "no
206
+ scopes". Additive and non-breaking; omitted when the token is available.
207
+
5
208
  ## 2.1.0
6
209
 
7
210
  ### Added
package/README.md CHANGED
@@ -1,141 +1,198 @@
1
- # ask-marcel-office-cli
1
+ <div align="center">
2
2
 
3
- **A Microsoft Graph CLI built for LLMs.** 180 commands across Mail, Calendar, OneDrive, SharePoint, Excel, Teams chats, Planner / To-Do, OneNote, and directory — plus local-file tools (markdown conversion, image extraction) that need no sign-in at all. Sign in once with your Microsoft 365 account — no Azure app registration, no admin consent, no client secrets.
3
+ # ask-marcel-office
4
4
 
5
- ```bash
6
- npm i -g ask-marcel-office-cli
7
- ask-marcel-office login # browser opens once, token cached
8
- ask-marcel-office my-quick-context # who am I + my IDs, in one round trip
9
- ask-marcel-office list-mail-messages --top 5
10
- ```
5
+ ### The Microsoft 365 command line built for AI agents
11
6
 
12
- ---
7
+ **One sign-in. 180 read-only commands. Every document as clean markdown.**
13
8
 
14
- ## Why it exists
9
+ [![npm version](https://img.shields.io/npm/v/ask-marcel-office-cli.svg?logo=npm&color=cb3837)](https://www.npmjs.com/package/ask-marcel-office-cli)
10
+ [![license: MIT](https://img.shields.io/npm/l/ask-marcel-office-cli.svg?color=blue)](LICENSE)
11
+ [![node ≥20](https://img.shields.io/node/v/ask-marcel-office-cli?logo=node.js&color=339933)](https://nodejs.org)
12
+ [![bun ≥1.0](https://img.shields.io/badge/bun-%E2%89%A51.0-14151a?logo=bun)](https://bun.sh)
13
+ [![types included](https://img.shields.io/npm/types/ask-marcel-office-cli?logo=typescript&color=3178c6)](docs/USAGE.md)
15
14
 
16
- LLM tool-loops keep hitting the same three walls with Microsoft Graph:
15
+ Outlook · OneDrive · SharePoint · Calendar · Excel · Teams · Planner · To Do · OneNote · People
17
16
 
18
- 1. **Auth is a project.** Register an app, get tenant-admin consent, manage secrets, refresh tokens before the first API call.
19
- 2. **Default payloads are tuned for backend services, not context windows.** Listing endpoints return every field on every item, used-range Excel calls return four redundant 2D arrays, attachment endpoints inline base64 by default. An agent that reads "what's in my inbox" without trimming burns its budget on metadata it never needed.
20
- 3. **Errors are opaque.** `BadRequest: Invalid filter clause` doesn't tell a model what to fix.
17
+ [Install](#install-in-60-seconds) · [See it work](#see-it-work) · [What it reaches](#what-your-agent-can-reach) · [Files to markdown](#any-file-becomes-markdown) · [Library](#embed-it-as-a-typescript-library) · [All 184 commands](docs/COMMANDS.md)
21
18
 
22
- `ask-marcel-office` fixes all three at the CLI layer, so the model just calls commands and reads back-pressure-friendly responses.
19
+ </div>
23
20
 
24
- ## What you get
21
+ ---
25
22
 
26
- ### Read-only by design
23
+ Your agent is smart enough to answer *"what did Contoso say about the Q3 budget, and what's in the attached deck?"* It just can't see your mailbox.
27
24
 
28
- **This is the most important property.** 173 GET endpoints + 3 read-only POST (two searches + free/busy lookup) + 3 POST (create mail draft / threaded reply draft / forward draft) + 1 PATCH (update draft) = 180 commands. No `send-mail`, no `create-event`, no `upload-file`, no `delete-anything`. The only write operations are draft creation (a new mail, a threaded reply-all, or a forward) and update — a hallucinated command can at most create an unsent draft in your Drafts folder. Safe default for autonomous agents, MCP servers, and "let Claude poke around my mailbox" sessions where you can't fully review every tool call.
25
+ `ask-marcel-office` gives any tool-calling LLM (Claude Code, Cursor, Cline, an MCP server, your own loop) eyes on your entire Microsoft 365:
29
26
 
30
- ### One call gets the full email context
27
+ ```bash
28
+ npm i -g ask-marcel-office-cli
29
+ ask-marcel-office login # your normal Microsoft sign-in, in a browser, once
30
+ ask-marcel-office list-mail-messages --top 5
31
+ ```
31
32
 
32
- A typical "read this email" loop in raw Graph: GET the message GET the attachments list GET each attachment's bytes scan the body HTML for `sharepoint.com` URLs resolve each URL to a driveItem GET each driveItem. Six round-trips minimum, plus HTML-to-text conversion the LLM has to do itself.
33
+ That is the whole setup. **No Azure app registration. No tenant-admin consent. No client secrets.** And nothing for a runaway agent to break: the command surface is read-only by design.
33
34
 
34
- `convert-mail-to-markdown` collapses that into one call:
35
+ ## The three walls it removes
35
36
 
36
- - Body rendered as markdown (turndown pipeline)
37
- - Quoted reply chains / forwarded-message blocks stripped by default so a long thread doesn't duplicate earlier messages into the model's context (the cut is replaced with a visible marker; opt out with `--keep-quoted true` to keep the full body)
38
- - Inline images embedded as base64 `data:` URIs (size-capped per image — opt out with `--inline-images false` to keep raw `cid:` refs)
39
- - File attachments listed below the body with id + name + size, ready for follow-up calls
40
- - Pair with `extract-sharepoint-links-in-mail` to resolve every SharePoint URL in the body to its driveItem in parallel (capped at 25 unique URLs per call)
37
+ ### 🔑 Sign in like a human, not like an app
41
38
 
42
- ### Office docs markdown or PDF on the fly
39
+ Microsoft Graph normally means registering an Azure app, chasing tenant-admin consent, and rotating client secrets before the first API call. Here, `login` drives a real browser window through the standard Microsoft sign-in (Playwright under the hood) and captures the same token the Teams web client already uses. Any Microsoft 365 account works, personal or enterprise. Tokens are cached at `~/.ask-marcel/token-cache.json` (0600) and refresh themselves headlessly; `scopes-check` reports per-token scopes and expiry with zero side effects.
43
40
 
44
- Feed any Office-shaped file (docx, xlsx, pptx, csv, rtf, odt, …) into the local conversion pipeline OR through Graph's `?format=pdf` when slide layout and images matter:
41
+ ### 🛡️ Safe to hand to an autonomous agent
45
42
 
46
- - `download-drive-item-as-markdown` docx via mammoth (embedded images become `[image]` placeholders by default — `--inline-images true` to embed them as base64, or pull the full-resolution originals with `extract-drive-item-images`), xlsx as one markdown table per sheet (a sheet whose used range exceeds the `--max-cells` cap, default 50 000, becomes a band-by-band read hint instead of a multi-hundred-MB table that would OOM), csv as a table, odt/ods/odp via content.xml (headings, lists, tables, named sheets, per-slide text, with `office:annotation` comments folded inline), **pptx** flattened to per-slide text (titles + bullets + text boxes + table cells, speaker notes inline, as `## Slide N` sections `download-drive-item-as-pdf` + a vision model when layout / images matter), **pdf** via text-layer extraction ([unpdf](https://github.com/unjs/unpdf) `text/plain`; a scanned / image-only PDF with no text layer points you at `download-drive-item-as-pdf` + a vision model), **legacy OLE Office** (`.xls` read by sheetjs like `.xlsx`; `.doc` extracted by [word-extractor](https://www.npmjs.com/package/word-extractor) as plain text; `.ppt` has no pure-JS path → convert to PDF first), plain-text passthrough
47
- - `download-drive-item-as-pdf` — Graph PDF conversion for anything it supports (preserves slide layout, images, charts — the right call for pptx and image-heavy docs)
48
- - `convert-mail-attachment-to-markdown` / `convert-mail-attachment-to-pdf` — same pipelines but starting from an email attachment
49
- - `read-mail-attachment` — one call that reads any mail attachment, auto-routing by file type (a `.zip` is unpacked and every entry converted; docx/xlsx/pptx/odf/csv/PDF/`.msg`/legacy/text → markdown; images, scanned PDFs, and legacy `.ppt` return an actionable 415 pointing at the raw-bytes / vision route) so an agent never has to choose between the `convert-mail-attachment-*` siblings
50
- - `convert-local-file` — same pipelines but starting from a file **on disk** (`--path ./report.docx`); never calls Graph (works offline, no login). A `.zip` is unpacked with every contained file converted in one call. The two things it can't do locally — convert **to** PDF and Loop/Fluid/Whiteboard sources — need Graph's server-side renderer (upload to OneDrive and use the drive-item siblings)
51
- - `convert-drive-item-zip` / `convert-mail-attachment-zip` — unzip an archive (OneDrive/SharePoint item, or an Outlook attachment) and convert **every** contained file in one call; legacy GBK / CP437 entry names (Chinese vendor archives from WinRAR / Windows Explorer) are decoded correctly, never mojibaked; unsupported entries are listed with a note instead of failing the archive
52
- - **Outlook `.msg` files** (saved/forwarded emails) convert to markdown through every entry point above — H1 subject, From/To/Cc/Date header block, the body, and an `## Attachments` section where each attachment is itself converted recursively (depth-capped)
53
- - `extract-sharepoint-links-in-documents` — the doc-side sibling of `extract-sharepoint-links-in-mail`: resolve every `*.sharepoint.com` URL embedded in a docx/xlsx/pptx (read from the package's relationship parts) or an odt/ods/odp (read from the inline `xlink:href` links in content.xml) to its driveItem, so an agent can follow references out of a document the same way it follows them out of an email
43
+ The 184 commands break down as 176 GET, 4 read-only POST (three searches and a free/busy lookup), and 4 mail-draft operations. No `send-mail`. No `create-event`. No `upload-file`. No `delete-anything`. The worst a hallucinated tool call can do is leave an unsent draft in your Drafts folder. That is the entire blast radius, which is why you can let an agent explore a mailbox without reviewing every call.
54
44
 
55
- Pass `--include-metadata true` on any `*-as-markdown` (or `convert-mail-attachment-to-markdown`) command to surface the side-channel content the rendered body hides. For **docx** (`## DOCX metadata`): core/app/custom doc properties, people registry, external hyperlinks, comments (each quoting the document text span it annotates), tracked changes, hidden text (`w:vanish`), MERGEFIELD / HYPERLINK / DOCVARIABLE instructions, bookmarks. For **xlsx** (`## Workbook metadata`): properties, external relationships, defined names, hidden / very-hidden sheets, legacy + threaded cell comments (each tagged with its cell), the persons registry. For **pptx** (`## PPTX metadata`): properties, external relationships, slide tags, comment authors + comments (legacy + modern, each anchored to its slide), and per-slide title / speaker notes / hidden flag — appended after the per-slide text body (use `download-drive-item-as-pdf` + a vision model for slide visuals / layout). Each family also covers its macro-enabled (`.docm` / `.xlsm` / `.pptm`) and template (`.dotx` / `.xltx` / `.potx`, etc.) variants, and surfaces a `### Macros (VBA)` section flagging an embedded `vbaProject.bin` (the file can execute code on open). For **OpenDocument** (`.odt` / `.ods` / `.odp`) the flag appends a `## OpenDocument metadata` block (Dublin Core + ODF properties, keywords, user-defined custom fields) after the converted body. No-op on other sources.
45
+ ### 🧠 Responses budgeted for a context window
56
46
 
57
- ### Extract embedded images from documents
47
+ Graph payloads are tuned for backend services. These are tuned for models. Listings return hand-picked fields instead of every property on every item (opt out with `--full true`). Email threads arrive with quoted reply chains stripped, which shrinks real threads by 78-90%. Inline images become `[inline image: logo.png]` placeholders instead of kilobytes of base64. Binaries over ~1 MB are refused inline and routed to disk via `--output-path`. And every failure returns `{error, hint, source, retryAfterSeconds}` so the model can repair its own call instead of retrying blind.
58
48
 
59
- `extract-drive-item-images` (OneDrive / SharePoint), `extract-mail-attachment-images` (Outlook attachments), and `extract-local-file-images` (a file **on disk** — no Graph, no login) pull the embedded images out of a **docx, xlsx, pptx, or pdf**. For Office files it reads the OOXML media parts (png/jpg/gif/bmp/tiff/webp/svg) — including original full-resolution / un-cropped originals and images on hidden slides that the rendered view never shows. SVG rides back as its XML source (which carries the diagram's own text labels); legacy vector (emf/wmf) and audio/video are skipped. For a PDF it walks every page via [unpdf](https://github.com/unjs/unpdf) (a pure-JS, no-native-deps pdf.js build) and re-encodes each painted image as PNG — page-oriented, so it captures images as drawn on each page (it does not reach layer-hidden/unpainted XObjects or the full uncropped original behind a clipped image). Pair with the global `--output-dir <dir>` to write every image to a folder (the directory is auto-created and each `base64` becomes a `savedTo` path); without it the bytes ride back base64-encoded so a vision model can read them directly. The same OOXML / PDF image extraction runs over every file **inside a `.zip`** when you pass `convert-local-file --include-images true`: each archive entry gains an `images` array, so a screenshot pasted into a zipped document is still reachable in one call.
49
+ ## See it work
60
50
 
61
- The CLI follows any SharePoint media-transform redirect internally, so the LLM never has to fetch an external URL.
51
+ Reading one email with its attachments in raw Graph: GET the message, GET the attachment list, GET each attachment's bytes, resolve every SharePoint link in the body, then run your own HTML-to-text pipeline. Six round trips minimum. Here:
62
52
 
63
- ### Find every drive you can reach
53
+ ```console
54
+ $ ask-marcel-office convert-mail-to-markdown --message-id "AAMkAD..."
64
55
 
65
- `list-drives` only returns your personal OneDrive(s). `list-accessible-drives` unions every discovery vector the delegated token can hit — `/me/drives` (personal), `/me/joinedTeams` (Teams libraries), `/me/memberOf` Unified groups → each group's drive (SharePoint M365-group sites), `/me/drive/sharedWithMe` (drives behind files shared with you), per-team `/teams/{id}/channels` → `filesFolder` for **private/shared channels** (which live in their own sites, not the team default drive), activity signals (`/me/drive/recent`, `/me/drive/following`, `/me/insights/{trending,used,shared}`), and every **non-default document library** of each discovered site via a path-addressed `/sites/{host}:/sites/{name}:/drives` (catches secondary libraries the default-drive vectors skip) — deduped by drive id and tagged with the `sources[]` that surfaced each one (`channel` = private/shared channel drive, `activity` = a recently-used/followed/trending item drive, `siteLibrary` = a non-default site library). These vectors catch OneDrives, channel sites, and direct-link sites the tenant search index (`search-sharepoint-sites-by-name`) never returns; the index in turn returns sites you can open but aren't a member of. For that index half, `search-all-accessible-sites` deep-pages the Microsoft Search API (`POST /search/query`, `entityTypes: ['site']`) past the single-page cap of `search-sharepoint-sites-by-name`, returning the *full* security-trimmed site index (on one tenant: ~154 sites vs 80). So **the union of `search-all-accessible-sites` + `list-accessible-drives` is the practical maximum on a delegated token** (truly enumerating *every* site in the tenant needs tenant-admin app-only `/sites/getAllSites`). Both site-search commands **exclude archived sites**: each result is probed (`GET /sites/{id}?$select=…,siteCollection`) and dropped when Graph reports it archived or fails with `423 resourceLocked` — the signal a departed/unlicensed user's auto-archived OneDrive returns (no more `sharepointerror.aspx?scenario=SiteArchived` dead links in the output); the count surfaces as `archivedExcluded`. `--max-groups` caps every fan-out, and `partialErrors[]` stays signal-only: benign "can't reach this one" results (404 no-drive, 403 access-denied / non-member channel, 423 admin-locked site, 400 stale id) are dropped silently — only actionable failures (auth, throttling, 5xx, network) are listed. Both commands also surface a best-effort `fileEstimate` — the Microsoft Search index's security-trimmed `driveItem` count, i.e. roughly how many files you can access across all of SharePoint/OneDrive (index-wide, not limited to the listed drives).
56
+ # Q3 budget review: action needed
57
+ **From:** Robin Chen <robin.chen@contoso.com>
66
58
 
67
- ### Browser-OAuth at first launch
59
+ Before Friday's review, the forecast tab still shows last quarter's
60
+ headcount. Can you sanity-check the attached numbers?
68
61
 
69
- No Azure app, no tenant admin. The CLI captures the same token the Teams web client uses — works for any Microsoft 365 account, personal or enterprise.
62
+ ## Attachments
63
+ - Q3-forecast.xlsx (48 KB) · id AAMkAD...
64
+ ```
70
65
 
71
- **Login flow:** the CLI drives a Playwright-launched Edge/Chrome window through the Teams sign-in, captures the tokens, and caches them at `~/.ask-marcel/token-cache.json` (0600). Running `login` again (even when already signed in) reports all four cached tokens — basic, elevated (M365), and the two Teams-chat substrate tokens (chatsvcagg / ic3) — each with its time-left and refresh route: basic/chatsvcagg/ic3 refresh automatically from the cached refresh token, while the elevated token is re-captured only on an interactive login. `login --force` ignores the cache and re-captures every token in one browser pass (the persistent profile is reused, so you are usually not re-prompted for credentials) — the only way to refresh the elevated token while the basic one is still valid. `scopes-check` reports the same four tokens without opening a browser.
66
+ The quoted chain below the reply is replaced by a one-line marker (restore it with `--keep-quoted true`), inline images become named placeholders, and attachments arrive with the ids ready for the follow-up call:
72
67
 
73
- ```bash
74
- ask-marcel-office login # sign in, or show all four tokens' status if already signed in
75
- ask-marcel-office login --force # re-capture every token, ignoring the cache
68
+ ```console
69
+ $ ask-marcel-office convert-mail-attachment-to-markdown \
70
+ --message-id "AAMkAD..." --attachment-id "AAMkAD..."
76
71
  ```
77
72
 
78
- ### Stable error envelope with actionable hints
79
-
80
- Every failure — Graph, CLI parser, Zod validation, substrate — comes back as `{ok: false, error, errorCode?, hint?, source, retryAfterSeconds?}`. The `hint` field tells the model *what to do next* (e.g. "string literals MUST use single quotes; embed one by doubling it") and `source` tells it where the failure came from. Curated rules for 20+ recurring Graph errors plus cross-resolver pointers (passed a Teams URL to `resolve-mail-link`? Hint says "re-run with `resolve-teams-link`"). When Graph throttles (HTTP 429, sometimes 503) the response's `Retry-After` is surfaced as `retryAfterSeconds` — the integer seconds to wait — so a caller running tenant-scale crawls can honor the server's backoff instead of guessing.
73
+ That xlsx comes back as one markdown table per sheet. And when the answer lives in a deck someone shared three weeks ago:
81
74
 
82
- ### Lean responses
75
+ ```console
76
+ $ ask-marcel-office microsoft-search-query --query "Q3 budget filetype:pptx"
77
+ $ ask-marcel-office download-drive-item-as-markdown --drive-id "b!abc..." --item-id "01BYE..."
78
+ ```
83
79
 
84
- Listings ship with hand-tuned `--select` defaults — a mail listing returns id, subject, from, to, cc, dates, read-state, importance, bodyPreview rather than every field on every message. `get-excel-used-range` returns the `values` array instead of the four 2D arrays Graph emits. Opt out per call with `--full true`, or override with your own `--select id,subject,body`.
80
+ ## The difference in practice
85
81
 
86
- ### Saves big binaries to disk so they never hit the model's context
82
+ | You need | Raw Microsoft Graph | ask-marcel-office |
83
+ |---|---|---|
84
+ | Access | App registration, admin consent, secret rotation | `login`: one browser sign-in with your own account |
85
+ | An email + attachments | 6+ round trips, HTML-to-text is your problem | `convert-mail-to-markdown`: one call, markdown out |
86
+ | A pptx / pdf / legacy .doc as text | Raw bytes, bring your own converter | `download-drive-item-as-markdown`: one call |
87
+ | A useful error | `BadRequest: Invalid filter clause` | `hint: "string literals MUST use single quotes; embed one by doubling it"` |
88
+ | A 5 MB PDF | Base64 flooding the context window | `--output-path` writes it to disk; the model reads 3 lines |
89
+ | Throttling | HTTP 429, guess the backoff | `retryAfterSeconds` surfaced in the error envelope |
90
+
91
+ ## What your agent can reach
92
+
93
+ | Surface | Commands | In practice |
94
+ |---|---:|---|
95
+ | 📧 Outlook Mail | 36 | Search and read mail as markdown, convert any attachment (down to nested `.zip` and `.msg`), resolve SharePoint links in bodies, extract your signature, find existing drafts on a thread, prepare reply / forward drafts (the only writes) |
96
+ | 📁 OneDrive + SharePoint | 49 | Discover every drive and site your token can reach, search files, read any document as markdown or PDF, version history, share links resolved even into partner tenants where you're a guest |
97
+ | 📅 Calendar | 24 | "What's on this week" via relative dates (`today`, `start-of-week`, `+7d`), event details, free/busy lookups |
98
+ | 👥 People + directory | 16 | People search, user profiles, the directory around you, your own identity and IDs in one round trip |
99
+ | 💬 Teams | 16 | Your teams, channels, chats, and message history |
100
+ | ✅ Planner + To Do | 15 | Plans, buckets, tasks, checklists, due dates |
101
+ | 📊 Excel | 11 | Live workbook reads: worksheets, used ranges, tables (lean values, not Graph's four redundant 2D arrays) |
102
+ | 📓 OneNote | 11 | Notebooks, sections, page content |
103
+ | 🔎 Search + utilities | 6 | Federated Microsoft Search across the tenant, cursor pagination (`next-page`), token status (`scopes-check`), offline local-file conversion |
104
+
105
+ Full per-command tables with required parameters and Graph endpoints: **[docs/COMMANDS.md](docs/COMMANDS.md)**.
106
+
107
+ ## Any file becomes markdown
108
+
109
+ One conversion pipeline, four entry points: a OneDrive / SharePoint item, an Outlook attachment, a `.zip` archive, or a file on disk (`convert-local-file-to-markdown` runs fully offline, no login).
110
+
111
+ | Source | What comes back |
112
+ |---|---|
113
+ | docx / docm / dotx | Clean markdown; `--include-metadata true` adds comments, tracked changes, hidden text, and a VBA-macro flag |
114
+ | xlsx / xlsm / csv | One markdown table per sheet, with a 50 000-cell guard so a huge sheet becomes a band-by-band read plan instead of an OOM |
115
+ | pptx / pptm | Per-slide text with speaker notes (`## Slide N`); switch to the PDF sibling + a vision model when layout matters |
116
+ | pdf | Text-layer extraction; a scanned PDF answers with a pointer to the vision route instead of silence |
117
+ | odt / ods / odp | Headings, lists, tables, per-slide text, comments folded inline |
118
+ | Legacy .doc / .xls | `.xls` reads like `.xlsx`; `.doc` extracts as plain text (`.ppt`: convert to PDF first) |
119
+ | Outlook .msg | The full email: headers, quote-stripped body, and every attachment converted recursively |
120
+ | .zip | Every entry converted in one call; GBK / CP437 entry names decoded, never mojibake |
121
+ | Loop / Whiteboard | Rendered through Graph's server-side converter |
122
+
123
+ Need the pictures instead of the words? `extract-drive-item-images`, `extract-mail-attachment-images`, and `extract-local-file-images` pull the embedded images out of docx / xlsx / pptx / pdf (including full-resolution originals and images on hidden slides), ready for a vision model; `--output-dir` writes them straight to disk.
124
+
125
+ ## Designed for the agent loop
126
+
127
+ - **Self-teaching.** `help-json --terse` returns a slim JSON manifest built for a model's first contact; add `--category mail` to scope it. `docs <command>` prints one command's full documentation (response shape, examples, the underlying Graph endpoint). Scan, pick, read, call.
128
+ - **Errors that repair the call.** Every failure is `{ok: false, error, errorCode?, hint?, source, retryAfterSeconds?}`, with curated hints for 20+ recurring Graph mistakes, including wrong-resolver pointers (a Teams URL passed to `resolve-mail-link` answers with the command to use instead).
129
+ - **Pagination without state.** Every listing returns an opaque cursor; `next-page --url "<cursor>"` continues any of them.
130
+ - **Relative dates.** `--start-date-time start-of-week --end-date-time +7d`. No timestamp math before "what's on my calendar".
131
+ - **Binary discipline.** Multi-MB payloads never hit stdout by accident: without `--output-path`, a binary answer is a one-line summary; with it, the bytes land on disk and the envelope carries `savedTo`.
132
+ - **Tenant-wide reach.** `list-accessible-drives` unions every discovery vector a delegated token can hit (Teams libraries, group sites, private channels, shared-with-me, activity signals, secondary site libraries) and `search-all-accessible-sites` deep-pages the search index. Together: the practical maximum reachable without tenant-admin rights.
133
+
134
+ ## Install in 60 seconds
87
135
 
88
- A typical conversion command returns multi-MB PDF bytes. Sending 5 MB of base64 through stdout would blow most context windows AND quadruple the token bill.
136
+ ```bash
137
+ # Bun >=1.0 or Node >=20 · macOS, Windows, Linux
138
+ npm i -g ask-marcel-office-cli
89
139
 
90
- Two flag patterns avoid the round-trip:
140
+ ask-marcel-office login # browser opens once, tokens cached
141
+ ask-marcel-office my-quick-context # who am I + my IDs, one round trip
142
+ ask-marcel-office list-calendar-view --start-date-time today --end-date-time +7d
143
+ ask-marcel-office search-onedrive-files --drive-id "b!abc..." --query "Q3 budget"
144
+ ```
91
145
 
92
- - **`--output-path /path/to/file.pdf`** the CLI decodes the bytes, writes them to disk, and replaces `base64: "..."` in the envelope with `savedTo: "/path/to/file.pdf"`. The LLM sees a 3-line confirmation instead of a 7-million-character payload. Works on every command that returns binary or text content; rejected with a clear error on plain-JSON commands so a misapplied flag is never silent. A binary payload over ~1 MB is **refused** without this flag (an `inline_too_large` error pointing you here), so a multi-MB base64 string can never flood the context by accident.
93
- - **No flag, text mode** — binary commands print a one-line summary (`binary: application/pdf, 4837291 bytes — use --output-path to save`) instead of spilling base64 to stdout. The LLM sees a hint without ever pulling the bytes.
146
+ ## Plug it into Claude Code, Cursor, Cline, or your own stack
94
147
 
95
- ### Relative dates on calendar windows
148
+ Any agent that can run a shell command can use the whole surface today: point it at `help-json --terse`, let it call commands with `--output json`, and the lean defaults plus structured hints let it recover from its own mistakes. No wrapper required.
96
149
 
97
- `--start-date-time "start-of-week" --end-date-time "+7d"`. No timestamp math before answering "what's on my calendar this week".
150
+ **No shell? Register it as an MCP server.** It speaks MCP over stdio, so any MCP client can drive it — straight off the npm registry via `npx`, nothing to clone or preinstall.
98
151
 
99
- ## 30-second quickstart
152
+ **Claude Code** — one command:
100
153
 
101
154
  ```bash
102
- # install (Bun ≥1.0 or Node ≥20)
103
- npm i -g ask-marcel-office-cli
104
-
105
- # authenticate (cached → refresh → browser fallback)
106
- ask-marcel-office login
107
-
108
- # the rest is read-only (the only writes are mail drafts) and discoverable from --help
109
- ask-marcel-office list-drives
110
- ask-marcel-office search-onedrive-files --drive-id "b!abc..." --query "Q3 budget"
111
- ask-marcel-office convert-mail-to-markdown --message-id "AAMkAD..."
112
- ask-marcel-office list-calendar-view --start-date-time today --end-date-time +7d
113
- ask-marcel-office convert-mail-attachment-to-pdf \
114
- --message-id "AAMkAD..." --attachment-id "AAMkAD...attach1" \
115
- --output-path /tmp/deck.pdf
155
+ claude mcp add --transport stdio --scope user ask-marcel-office -- npx -y ask-marcel-office-cli mcp
116
156
  ```
117
157
 
118
- ## Asking the CLI what it can do
158
+ **Claude Desktop, Cursor, or any other MCP client** — they all take the same `mcpServers` block; only the file location differs:
159
+
160
+ | Client | Config file |
161
+ |:--|:--|
162
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
163
+ | Claude Desktop (Windows) | `%AppData%\Claude\claude_desktop_config.json` |
164
+ | Cursor, Cline, Windsurf, … | see your client's MCP docs — the block below is unchanged |
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "ask-marcel-office": {
170
+ "description": "Ask Marcel Office CLI MCP server",
171
+ "command": "npx",
172
+ "args": ["-y", "ask-marcel-office-cli", "mcp"],
173
+ "env": {}
174
+ }
175
+ }
176
+ }
177
+ ```
119
178
 
120
- Five discovery surfaces, each tuned for a different audience and token budget:
179
+ > **Two things to know.** The first launch downloads the package into npx's cache, so allow it one slow start; every start after that is instant. And GUI apps don't inherit your shell's `PATH`: terminal-launched clients resolve `"npx"` fine, but a client launched from the Dock or Start menu (Claude Desktop) may not find it — if the server won't start, replace `"command": "npx"` with the absolute path `which npx` prints. Bun users: `bunx` works in place of `npx -y`.
121
180
 
122
- | When you want | Run | Returns |
123
- |---|---|---|
124
- | Help with a single command | `ask-marcel-office <command> --help` | Required flags, optional flags, an example, pagination notes |
125
- | A scan of every command | `ask-marcel-office --help` | One-sentence summary per command, grouped by category |
126
- | The slim LLM-friendly index | `ask-marcel-office help-json --terse` | JSON manifest with heavy fields (options, response shape) stripped — best first-call for an agent meeting the CLI for the first time |
127
- | The slim index for one domain | `ask-marcel-office help-json --terse --category mail` | Same as above, filtered to one of 12 categories — keeps the response tiny when the agent already knows the domain |
128
- | Rich docs for one command | `ask-marcel-office docs <command>` | Full Markdown to stdout (response shape, examples, the underlying Graph endpoint, Microsoft Learn link) |
181
+ You get **five gateway tools**, not one per command — 184 tool schemas would bloat every session, the opposite of the point:
129
182
 
130
- Pair `help-json --terse --category <name>` with `docs <command>` for the canonical agent loop: scan the category, pick a command, fetch its full docs, then call it.
183
+ | Tool | Does |
184
+ |:--|:--|
185
+ | `list-commands` | The terse manifest. Start here; `category` narrows it. |
186
+ | `get-command-docs` | Full docs for one command: options, endpoint, example. |
187
+ | `run-command` | The 180 **read** commands. `readOnlyHint: true`, so clients can auto-approve it. |
188
+ | `run-write-command` | The 4 mail-draft **write** commands. Separate tool so the read tool's promise stays honest. |
189
+ | `login` | Sign in / refresh. Opens a browser on this machine. |
131
190
 
132
- ## Use it from Claude Code, Cursor, Cline, or any tool-calling LLM
191
+ **Sign in from a terminal first** `npx -y ask-marcel-office-cli login`. Do this once before wiring up any client, and the reads just work.
133
192
 
134
- Most agents already know how to read JSON from stdout. Two patterns work:
193
+ ## Embed it as a TypeScript library
135
194
 
136
- **1. Drop in as a shell tool** — the agent learns the manifest, then runs `ask-marcel-office <command> --output json`. The slim defaults + structured error hints mean it can self-recover from typos.
137
-
138
- **2. Embed as a library** — every command is exported. Compose it inside your own MCP server, Claude Agent, or LangChain tool:
195
+ Every command is exported, typed, and returns `Result<T, E>` (no thrown surprises). Compose them inside your own agent or LangChain tool:
139
196
 
140
197
  ```ts
141
198
  import { commands, buildDeps } from 'ask-marcel-office-cli';
@@ -143,57 +200,59 @@ import { commands, buildDeps } from 'ask-marcel-office-cli';
143
200
  const { graph } = buildDeps();
144
201
  const result = await commands['list-mail-messages'].execute(graph, { top: '10' });
145
202
  if (result.ok) {
146
- // result.value is the Graph payload — typed Result<unknown, GraphError>
203
+ // result.value is the Graph payload
147
204
  }
148
205
  ```
149
206
 
150
- ### Auth two paths
151
-
152
- **Most users — use the built-in browser-OAuth ladder:**
153
-
154
- ```ts
155
- import { buildDeps } from 'ask-marcel-office-cli';
156
-
157
- const { graph } = buildDeps();
158
- // First call triggers cache → refresh → headed-Chromium fallback automatically.
159
- // Tokens cached at ~/.ask-marcel/token-cache.json for subsequent calls.
160
- ```
161
-
162
- **Agents / CI / MCP servers — bring your own token:**
207
+ **Bring your own token** (CI, MCP servers, vault-managed environments): swap the built-in browser auth for any token source via `createGraphClient`.
163
208
 
164
209
  ```ts
165
210
  import { createGraphClient } from 'ask-marcel-office-cli';
166
211
 
212
+ const cancelled = async () => ({ ok: false as const, error: { type: 'auth_cancelled' as const } });
213
+
167
214
  const graph = createGraphClient({
168
- getAccessToken: async () => ({
169
- ok: true,
170
- value: await fetchTokenFromYourVault(),
171
- }),
215
+ getAccessToken: async () => ({ ok: true, value: await fetchTokenFromYourVault() }),
172
216
  logout: async () => ({ ok: true, value: undefined }),
217
+ // Decline the token tiers your source cannot mint; only the commands
218
+ // that need them will notice, and they fail with a clear message.
219
+ getElevatedAccessToken: cancelled, // ODSP-gated: historical-version downloads
220
+ getGuestAccessToken: cancelled, // partner tenants you are a guest in
221
+ getChatsvcaggAccessToken: cancelled, // Teams chat substrate
222
+ getIc3AccessToken: cancelled, // Teams chat history
223
+ getChatsvcaggRegion: async () => 'emea',
224
+ getLastElevatedOutcome: () => null,
225
+ getLastChatsvcaggOutcome: () => null,
173
226
  });
174
227
  ```
175
228
 
176
- The `AuthManager` interface is two async methods that return `Result<T, AuthError>`. Plug in any token source — Azure Managed Identity, a secrets vault, an on-behalf-of flow, hand-pasted JWTs in tests. The Graph client doesn't care where the token came from.
229
+ Azure Managed Identity, an on-behalf-of flow, hand-pasted JWTs in tests: the Graph client doesn't care where the token came from. Full library guide: **[docs/USAGE.md](docs/USAGE.md)**.
177
230
 
178
231
  ## Deep docs
179
232
 
180
- - **[All 180 commands](docs/COMMANDS.md)** per-category tables with required params + Graph endpoint
181
- - **[Usage guide](docs/USAGE.md)** output formats, OData passthrough, `--output-path`, pagination, library API, architecture, configuration, quality gates
182
- - **[Machine-readable manifest](docs/commands.json)** JSON for programmatic discovery (LLM tool-loops, IDE plugins, MCP servers); also importable via `import manifest from 'ask-marcel-office-cli/commands.json'`
183
- - **[QA playbook](docs/QA-PLAYBOOK.md)** the repeatable full-surface health-check procedure (offline gates, parameter matrix, conversion contracts, live Graph drift probes) used to audit each release
233
+ - **[All 184 commands](docs/COMMANDS.md)**: per-category tables with required params + Graph endpoint
234
+ - **[Usage guide](docs/USAGE.md)**: output formats, OData passthrough, `--output-path`, pagination, library API, architecture, configuration
235
+ - **[Machine-readable manifest](docs/commands.json)**: JSON for programmatic discovery, also importable via `import manifest from 'ask-marcel-office-cli/commands.json'`
236
+ - **[QA playbook](docs/QA-PLAYBOOK.md)**: the repeatable full-surface health check run before each release
184
237
 
185
238
  ## Roadmap
186
239
 
187
- Read-only stays the default forever. There's no fixed feature backlog — coverage grows out of real LLM workflows as they come up.
188
-
189
- Suggestions, requests, and pull requests welcome — see the [issues page](https://github.com/vdelacou/ask-marcel-office-cli/issues).
240
+ Read-only stays the default forever. Coverage grows out of real LLM workflows as they come up: suggestions, requests, and pull requests welcome on the [issues page](https://github.com/vdelacou/ask-marcel-office-cli/issues).
190
241
 
191
242
  ## Built with
192
243
 
193
- - **Bun + TypeScript** single binary install, Node 20 fallback. `Result<T, E>` at every IO boundary, branded value-object types at trust boundaries, classicist outside-in TDD, zero lint warnings, 100% coverage on every tier.
194
- - **Microsoft Graph v1.0** the public API surface, no beta endpoints in production code.
195
- - **Playwright** headed Chromium for the first-launch browser-OAuth dance.
244
+ - **Bun + TypeScript**: single-package install, Node >=20 fallback, `Result<T, E>` at every IO boundary, a 100% coverage gate on every tier
245
+ - **Microsoft Graph v1.0**: the public API surface, no beta endpoints in production code
246
+ - **Playwright**: the headed browser behind the one-time sign-in
196
247
 
197
248
  ## License
198
249
 
199
250
  MIT © Vincent Delacourt
251
+
252
+ ---
253
+
254
+ <div align="center">
255
+
256
+ **If this saved your agent a few round trips, a ⭐ helps other agents' humans find it.**
257
+
258
+ </div>