ask-marcel-office-cli 2.3.0 → 2.5.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 (33) hide show
  1. package/CHANGELOG.md +274 -0
  2. package/README.md +45 -26
  3. package/dist/cli.js +3681 -2370
  4. package/dist/commands.json +481 -30
  5. package/dist/composition/run-registry-command.d.ts +4 -4
  6. package/dist/domain/utilities/base64.d.ts +11 -0
  7. package/dist/index.js +2441 -1638
  8. package/dist/infra/auth.d.ts +30 -4
  9. package/dist/infra/browser-auth.d.ts +21 -1
  10. package/dist/presenter/output-text.d.ts +1 -1
  11. package/dist/presenter/output.d.ts +2 -2
  12. package/dist/presenter/render-to-string.d.ts +14 -6
  13. package/dist/use-cases/commands/convert-group-post-attachment-to-markdown.d.ts +21 -0
  14. package/dist/use-cases/commands/convert-group-post-to-markdown.d.ts +20 -0
  15. package/dist/use-cases/commands/convert-mail-attachment-to-markdown.d.ts +6 -3
  16. package/dist/use-cases/commands/convert-mail-to-markdown.d.ts +35 -3
  17. package/dist/use-cases/commands/docx-metadata.d.ts +33 -2
  18. package/dist/use-cases/commands/get-group-post-attachment.d.ts +13 -0
  19. package/dist/use-cases/commands/get-group-post.d.ts +4 -0
  20. package/dist/use-cases/commands/include-hidden-folders.d.ts +2 -0
  21. package/dist/use-cases/commands/list-group-post-attachments.d.ts +4 -0
  22. package/dist/use-cases/commands/list-group-thread-posts.d.ts +4 -0
  23. package/dist/use-cases/commands/list-shared-mailbox-child-folders.d.ts +4 -0
  24. package/dist/use-cases/commands/list-shared-mailbox-folders.d.ts +4 -0
  25. package/dist/use-cases/commands/login-status.d.ts +2 -0
  26. package/dist/use-cases/commands/login.d.ts +18 -8
  27. package/dist/use-cases/commands/next-page.d.ts +1 -0
  28. package/dist/use-cases/commands/ooxml-xml-walker.d.ts +31 -6
  29. package/dist/use-cases/commands/pptx-comments.d.ts +8 -5
  30. package/dist/use-cases/commands/token-tier-capability.d.ts +5 -0
  31. package/docs/COMMANDS.md +20 -12
  32. package/docs/USAGE.md +8 -8
  33. package/package.json +26 -9
package/CHANGELOG.md CHANGED
@@ -2,6 +2,280 @@
2
2
 
3
3
  All notable changes to `ask-marcel-office-cli` are documented here.
4
4
 
5
+ ## 2.5.0
6
+
7
+ ### Fixed: `help` costs the same whoever runs it
8
+
9
+ The top-level listing is compacted to fit an LLM's token budget, and then
10
+ Commander sized its description column from `process.stdout.columns`, so the
11
+ budget depended on the reader's terminal. Measured on this surface: 35 KB at 80
12
+ columns, 64 KB at 100, 46 KB at 140, 39 KB at 200. Not even monotonic, so no
13
+ width was safe to assume.
14
+
15
+ The wrap width is now pinned at 80 and the listing is 35 KB everywhere.
16
+
17
+ This also broke `npm publish`. The byte-count guard in `cli.test.ts` asserts the
18
+ listing stays under 45 KB, and under CI or any piped run stdout is not a TTY, so
19
+ Commander fell back to 80 and the guard passed. Run from an ordinary terminal at
20
+ 90 to 150 columns it failed, which is where most terminals sit. A new test
21
+ renders the listing at four widths and asserts one size, so the guard can no
22
+ longer measure a different thing for each reader.
23
+
24
+ ### Fixed: the group conversation collections stop advertising a filter Graph refuses
25
+
26
+ `list-group-threads` and `list-group-conversations` handed out the full OData
27
+ option set, so both advertised `--filter`. Graph accepts it on neither. Probed
28
+ live on a group the signed-in user belongs to: any predicate on the threads
29
+ collection answers `ConversationFilterOther`, `isLocked eq false` answers
30
+ `ConversationFilterIsLockedEqualsFalse` because the only accepted forms are
31
+ `IsLocked eq true` and `IsLocked ne false`, and a predicate on conversations
32
+ answers `ErrorUnsupportedPathForQuery`.
33
+
34
+ That broke the promise the usage guide makes, that the CLI advertises only the
35
+ flags the endpoint honours so the manifest never lies. An agent reading the
36
+ manifest spent a round trip to find out. `--filter` is dropped from both; the
37
+ flag is now refused locally with the unknown-flag hint and no network call.
38
+ Filtering a group inbox down to its locked threads is not a use this CLI has,
39
+ so the one working predicate did not earn a caveat in its place.
40
+
41
+ `--top`, `--skip`, `--orderby`, `--select` and `--expand` are all honoured on
42
+ both collections and are unchanged.
43
+
44
+ ### Added: docx tracked changes cover the table
45
+
46
+ `extractDocxMetadata` reported formatting revisions on runs and paragraphs only,
47
+ so a reviewer who restyled a table left no trace in the metadata block. Word
48
+ records a table, row and cell property change the same way it records a run's,
49
+ the properties as they were BEFORE the edit nested inside a `*Change` element,
50
+ so the same walker reads all five. `formatChanges[].scope` gains `table`, `row`
51
+ and `cell` alongside `run` and `paragraph`.
52
+
53
+ One limit worth knowing: a property whose values hang off child elements rather
54
+ than its own attributes, and `w:tblBorders` is the common table case, compares
55
+ as unchanged. The revision is still reported with its author, date and scope; it
56
+ names no property. Structural revisions (`w:cellIns`, `w:cellDel`, `w:cellMerge`)
57
+ carry no before/after pair and are not reported.
58
+
59
+ ### Added: a gate that holds the docs to the registry's numbers
60
+
61
+ Four numeric claims drifted during the 2.4.0 cycle, and one was wrong on main for
62
+ two releases: `docs/USAGE.md` promised "the 180 READ commands" where the registry
63
+ held 188, three sections after `README.md` said 188 correctly. `mcp.ts` already
64
+ derives its counts and carries a comment saying the prose must be derived too;
65
+ nothing enforced it.
66
+
67
+ `bun run check:docs` reads the counts off the command registry and the rendered
68
+ manifest, then holds all ten numeric claims in `README.md`, `docs/USAGE.md` and
69
+ `docs/COMMANDS.md` to them, printing `file:line expected N, found M`. A claim
70
+ whose anchor text no longer matches anything fails too, so rewording a sentence
71
+ cannot silently switch its own check off. It runs in CI behind a `--selftest`
72
+ that proves the matcher rejects both a wrong number and a lost anchor.
73
+
74
+ The stale 188 is corrected in the same change.
75
+
76
+ ### Added: read what is attached to a group post
77
+
78
+ `get-group-post --expand attachments` was the only route to a post's attachments,
79
+ and it inlines every one of them at once, which is the shape that times out on a
80
+ post carrying a multi-MB file. Three commands make the group inbox readable
81
+ attachment by attachment, all on the `Group.Read.All` scope the token already has:
82
+
83
+ - `list-group-post-attachments` returns the metadata alone, with the same slim
84
+ default `--select` the mail and calendar siblings use. Graph silently ignores
85
+ `$top`, `$skip`, `$orderby` and `$filter` here (probed live: asking for one
86
+ attachment returned all seven, and `isInline eq false` returned all seven when
87
+ every one was inline), so only `--select` and `--expand` are exposed.
88
+ - `get-group-post-attachment` fetches one attachment, mirroring `contentBytes`
89
+ as `base64` so `--output-path` lands it on disk. This is also the route to an
90
+ image attached to a post.
91
+ - `convert-group-post-attachment-to-markdown` renders one through the shared
92
+ conversion pipeline, the way the mail and calendar siblings do.
93
+
94
+ ### Fixed: unconvertible attachments name a command that can reach them
95
+
96
+ The shared conversion pipeline hardcoded the mail remediation wording, so an
97
+ image or scanned PDF attached to a calendar event told the caller to run
98
+ `get-mail-attachment --message-id ...`, which cannot address an event. Each
99
+ caller now supplies its own hints: the calendar converter points at
100
+ `get-calendar-event --expand attachments` and its PDF sibling, and the new post
101
+ converter points at `get-group-post-attachment`.
102
+
103
+ ### Fixed: inline-only mail no longer hides its images
104
+
105
+ Graph reports `hasAttachments: false` for a message whose only attachments are
106
+ inline images, and both markdown converters fetched the attachment list only on
107
+ that flag. Such a message rendered its images as unnamed placeholders, left them
108
+ out of the attachments list, and embedded nothing under `--inline-images true`.
109
+ The list is now fetched whenever the body references a `cid:` image as well,
110
+ the guard `get-mail-signature` already uses, so
111
+ `convert-mail-to-markdown` and `convert-group-post-to-markdown` name the
112
+ placeholders, list the images with their ids, and embed them on request. No
113
+ extra call for a message without inline images.
114
+
115
+ ### Changed: the shared-mailbox commands say when they will 403
116
+
117
+ `list-shared-mailbox-messages`, `list-shared-mailbox-folder-messages`,
118
+ `list-shared-mailbox-folders`, `list-shared-mailbox-child-folders` and
119
+ `get-shared-mailbox-message` used to promise a 403 only "if the signed-in user
120
+ does not have shared access". The scope they need, `Mail.Read.Shared`, is on
121
+ neither token this CLI can mint, so their summaries now state that any mailbox
122
+ other than your own is expected to answer `ErrorAccessDenied` whatever delegation
123
+ Exchange holds, that your own UPN works, and that a Microsoft 365 group's mailbox
124
+ is the shared-mail path that does.
125
+
126
+ ### Added: read the posts of a Microsoft 365 group conversation
127
+
128
+ `list-group-conversations` and `list-group-threads` stopped at Graph's truncated
129
+ `preview`, and a group mailbox cannot be read through the shared-mailbox commands
130
+ (`ErrorGroupIsUsedInNonGroupURI`), so a group's mail was reachable up to its titles
131
+ and no further. Three commands close the gap, all on the `Group.Read.All` scope the
132
+ token already carries:
133
+
134
+ - `list-group-thread-posts --group-id --thread-id` returns every `post` of a thread
135
+ with its HTML body, `from`, `sender`, `receivedDateTime` and `hasAttachments`.
136
+ Graph hands back the whole thread in one call and silently ignores `$top`, `$skip`
137
+ and `$orderby` while rejecting `$filter` (probed live), so only `--select` and
138
+ `--expand` are exposed and there is no page cursor.
139
+ - `get-group-post --group-id --thread-id --post-id` is the `get-mail-message` sibling
140
+ for a group inbox; `--expand attachments` inlines the attachments with their bytes.
141
+ - `convert-group-post-to-markdown` renders one post the way `convert-mail-to-markdown`
142
+ renders a message, with the same `--inline-images` and `--keep-quoted` flags. A post
143
+ arrives from the group's own address with the writer in `sender`, so the author line
144
+ reads `Robin Chen <...> on behalf of Support <...>`; the thread `topic` is the subject
145
+ and is not repeated.
146
+
147
+ The mail-to-markdown pipeline now takes the resource path and the header renderer as
148
+ parameters; the mail command's output is unchanged. `list-group-threads` and
149
+ `list-group-conversations` point at the reader, and at `--expand posts`, which already
150
+ inlined the bodies.
151
+
152
+ ### Fixed — deleted text no longer renders in an `.odt` body
153
+
154
+ The body converter walked `text:tracked-changes` like any other block, so the
155
+ paragraphs a reviewer had deleted printed as if they were still in the
156
+ document. The region is skipped, so the body reads as the document stands.
157
+
158
+ ## 2.4.0
159
+
160
+ Two community fixes from [@a-tokyo](https://github.com/a-tokyo). Nothing breaks:
161
+ the new flag is optional, and every existing invocation routes exactly as before.
162
+
163
+ ### Fixed — share links whose filename is not ASCII
164
+
165
+ `buildShareToken` fed the raw URL string to `btoa`, which only accepts Latin-1.
166
+ A sharing URL with an accented filename minted a token that resolved to nothing,
167
+ and one with a CJK or emoji filename threw `InvalidCharacterError`, which crashed
168
+ the command from inside `Promise.all`. The URL's UTF-8 bytes are now encoded
169
+ before base64url, which is what Graph's `/shares/{token}` resolver expects.
170
+
171
+ Six commands build that token and all six were affected:
172
+
173
+ | Command | How it reaches the token |
174
+ |---------|--------------------------|
175
+ | `resolve-drive-share-link` | encodes the sharing URL directly |
176
+ | `convert-mail-attachment-to-markdown` | resolves an attachment's source URL |
177
+ | `convert-mail-attachment-to-pdf` | resolves an attachment's source URL |
178
+ | `extract-mail-attachment-images` | resolves an attachment's source URL |
179
+ | `extract-sharepoint-links-in-mail` | one resolve per link found in the body |
180
+ | `extract-sharepoint-links-in-documents` | one resolve per link found in the text |
181
+
182
+ The byte-to-base64 step moved into a shared `bytesToBase64` codec under
183
+ `domain/utilities`, replacing the private copy that lived in `fetch-raw-bytes`.
184
+
185
+ ### Added — `next-page --tenant-id`
186
+
187
+ `next-page` re-signs a cursor to match the identity its originating command used,
188
+ but it knew only two: the home basic token and the elevated chat token. A
189
+ partner-tenant drive listing signs page 1 with a guest token, and the
190
+ `@odata.nextLink` it emits carries no tenant, so page 2 was re-signed with the
191
+ home token and Graph answered `401 invalidAudienceUri`. Anything longer than one
192
+ page in a partner tenant was unreadable past page 1.
193
+
194
+ Pass the same `--tenant-id` you gave the originating command (ultimately from
195
+ `resolve-drive-share-link`) and the cursor is re-signed with a guest token for
196
+ that tenant. The flag reuses the existing `tenant-option` branding, so a
197
+ malformed GUID is rejected at the boundary before any Graph call.
198
+
199
+ The footer carries the flag for you. `next:` and `delta:` both render
200
+ `--tenant-id <guid>` whenever the call that produced the cursor used one, so the
201
+ line stays copy-pasteable rather than becoming a 401 one step later. This is the
202
+ line an MCP agent reads to build its next call, not just a terminal convenience.
203
+
204
+ ### Added — docx tracked changes report replacements as one edit
205
+
206
+ OOXML has no "replace" revision: Word records replacing a span as a deletion
207
+ sitting next to an insertion. Reported as two loose halves, one edit read as an
208
+ unrelated cut plus an unrelated addition somewhere else, with nothing linking
209
+ them. A deletion and an insertion that are adjacent siblings by the same author
210
+ now come back as a single `replacements` entry carrying `before` and `after`,
211
+ and the two halves leave `insertions` / `deletions` so nothing is counted twice.
212
+ The markdown output gains a `### Tracked changes — replacements` section above
213
+ the two existing ones.
214
+
215
+ Adjacency is real document order, not a guess. The default XML parse groups
216
+ same-named siblings per tag, which cannot distinguish del, ins, del, ins from
217
+ del, del, ins, ins, so revisions are re-read through a second order-preserving
218
+ parser. Pairing is scoped to siblings of one parent, requires the same author on
219
+ both halves, and treats Word's glyph-free markers (`w:proofErr`, bookmark and
220
+ comment anchors) as not separating a pair, while any untouched run of prose does.
221
+
222
+ Still unreported, unchanged by this: `w:moveFrom` / `w:moveTo`, `w:rPrChange`,
223
+ `w:pPrChange`, and the table-structure revisions. A document whose only tracked
224
+ edit is a formatting change still reports no tracked changes at all.
225
+
226
+ ### Added — docx moves and formatting changes are reported at all
227
+
228
+ A docx whose only tracked edits were a moved paragraph or a reformat reported
229
+ no tracked changes whatsoever, which reads as a clean document rather than an
230
+ unreviewed one. `w:moveFrom` / `w:moveTo` and `w:rPrChange` / `w:pPrChange` are
231
+ now extracted into two new sections.
232
+
233
+ `moves` joins the two ends of a move through the `w:name` on the range-start
234
+ markers that bracket them, not by matching their text: Word does guarantee the
235
+ halves carry identical content, so text-matching looks free right up to a
236
+ document that moves the same sentence twice and silently cross-pairs four halves
237
+ into two moves that never happened. A half whose partner is missing still gets a
238
+ row, marked `from-only` or `to-only`.
239
+
240
+ `formatChanges` names which properties a reviewer altered, comparing attributes
241
+ as well as tag names. `w:color` sits on both sides of a recolour with only its
242
+ `w:val` moving, so a comparison of tag names alone calls that run unchanged.
243
+ Run-level and paragraph-level changes share one list, separated by `scope`.
244
+
245
+ Table-structure revisions (cell insert / delete / merge, numbering, section and
246
+ table properties) remain unreported, and the `--include-metadata` docs now say
247
+ so rather than leaving it to be discovered.
248
+
249
+ ### Added — `get-mail-signature` empty states carry an `errorCode`
250
+
251
+ Finding no signature used to come back as a validation error with a good
252
+ message and no code, so an agent routing on `errorCode` saw nothing and the
253
+ live QA sweep logged it as `ERR:err`. Two codes now name the two distinct
254
+ misses: `signature_not_found` when the scanned or pinned message carries no
255
+ OWA signature block, and `no_sent_messages` when there is nothing to scan at
256
+ all. The messages are unchanged.
257
+
258
+ ### Fixed — `scopes-check` never opens a browser
259
+
260
+ `scopes-check` obtained the basic token through the same acquiring getter as
261
+ every other command, and in a terminal that getter heals an expired session by
262
+ opening a browser; when the persistent profile is already signed in it also
263
+ clears the session so the OAuth grant can re-fire, which drops the 90-day
264
+ "Stay signed in" cookie. A diagnostic did all of that on 2026-09-02. The basic
265
+ tier is now read straight off the cache like the elevated and Teams tiers
266
+ already were: an expired token reports a negative `expiresInSeconds`, an empty
267
+ cache reports not-signed-in, and `login` remains the command that refreshes.
268
+ Bring-your-own-token managers built with `createGraphClient` are unaffected.
269
+
270
+ ### Added — an `invalidAudienceUri` error hint
271
+
272
+ The one 401 that re-authenticating cannot fix now says so. Graph cannot mint a
273
+ SharePoint token for a tenant you are only a guest in, so an agent told to run
274
+ `login` here loops forever. The hint names `--tenant-id`, points at
275
+ `resolve-drive-share-link` for the GUID, and covers the paginated case where the
276
+ cursor carries no tenant of its own. It sits above the token-expiry rule, which
277
+ would otherwise claim the same 401 and give the wrong advice.
278
+
5
279
  ## 2.3.0
6
280
 
7
281
  > **Read this before upgrading.** This release contains breaking changes to the
package/README.md CHANGED
@@ -4,17 +4,15 @@
4
4
 
5
5
  ### The Microsoft 365 command line built for AI agents
6
6
 
7
- **One sign-in. 180 read-only commands. Every document as clean markdown.**
7
+ **One sign-in. No Azure app registration. Every document as clean markdown.**
8
8
 
9
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
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
11
  [![types included](https://img.shields.io/npm/types/ask-marcel-office-cli?logo=typescript&color=3178c6)](docs/USAGE.md)
14
12
 
15
13
  Outlook · OneDrive · SharePoint · Calendar · Excel · Teams · Planner · To Do · OneNote · People
16
14
 
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)
15
+ [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 192 commands](docs/COMMANDS.md)
18
16
 
19
17
  </div>
20
18
 
@@ -32,15 +30,38 @@ ask-marcel-office list-mail-messages --top 5
32
30
 
33
31
  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.
34
32
 
33
+ <p align="center">
34
+ <img src="https://raw.githubusercontent.com/ask-marcel/ask-marcel-office-cli/main/docs/demo.gif" alt="ask-marcel-office converting an Outlook .msg file into clean markdown in one command, offline" width="880">
35
+ </p>
36
+
37
+ ## The difference in practice
38
+
39
+ | You need | Raw Microsoft Graph | ask-marcel-office |
40
+ |---|---|---|
41
+ | Access | App registration, admin consent, secret rotation | `login`: one browser sign-in with your own account |
42
+ | An email + attachments | 6+ round trips, HTML-to-text is your problem | `convert-mail-to-markdown`: one call, markdown out |
43
+ | A pptx / pdf / legacy .doc as text | Raw bytes, bring your own converter | `download-drive-item-as-markdown`: one call |
44
+ | A useful error | `BadRequest: Invalid filter clause` | `hint: "string literals MUST use single quotes; embed one by doubling it"` |
45
+ | A 5 MB PDF | Base64 flooding the context window | `--output-path` writes it to disk; the model reads 3 lines |
46
+ | Throttling | HTTP 429, guess the backoff | `retryAfterSeconds` surfaced in the error envelope |
47
+
48
+ And against the tools you already know:
49
+
50
+ | | Microsoft Graph SDKs | CLI for Microsoft 365 (PnP) | ask-marcel-office |
51
+ |---|---|---|---|
52
+ | Sign-in | An Entra app registration, always: every auth flow needs a client id | An Entra app registration; its `setup` command creates one, and the permissions it requests can need admin consent | Your own account, in a browser, once. No app registration |
53
+ | Built for | Backend services | Admins and scripts | Tool-calling LLMs: markdown out, lean fields, a repair hint on every error |
54
+ | Can it break things | Whatever your app's permissions allow | Yes: it creates, changes, and deletes across the tenant | No: read-only by design; the four writes only leave unsent drafts |
55
+
35
56
  ## The three walls it removes
36
57
 
37
58
  ### 🔑 Sign in like a human, not like an app
38
59
 
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.
60
+ 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 without a Graph call and without ever opening a browser: an expired session is reported as expired, not re-captured.
40
61
 
41
62
  ### 🛡️ Safe to hand to an autonomous agent
42
63
 
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.
64
+ The 192 commands break down as 184 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. No analytics, either: the only outbound traffic is Microsoft Graph and a periodic npm version check.
44
65
 
45
66
  ### 🧠 Responses budgeted for a context window
46
67
 
@@ -53,17 +74,26 @@ Reading one email with its attachments in raw Graph: GET the message, GET the at
53
74
  ```console
54
75
  $ ask-marcel-office convert-mail-to-markdown --message-id "AAMkAD..."
55
76
 
56
- # Q3 budget review: action needed
77
+ **Subject:** Q3 budget review: action needed
57
78
  **From:** Robin Chen <robin.chen@contoso.com>
79
+ **To:** Alex Kim <alex.kim@contoso.com>
80
+ **Date:** 2026-09-02T08:14:22Z
58
81
 
59
82
  Before Friday's review, the forecast tab still shows last quarter's
60
83
  headcount. Can you sanity-check the attached numbers?
61
84
 
62
- ## Attachments
63
- - Q3-forecast.xlsx (48 KB) · id AAMkAD...
85
+ Robin
86
+
87
+ \[inline image: contoso-signature.png\]
88
+
89
+ _\[Quoted reply chain removed — pass --keep-quoted true to include it\]_
90
+
91
+ **Attachments:**
92
+ - Q3-forecast.xlsx (49.2 KB, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, id: AAMkAD...)
93
+ _Use `convert-mail-attachment-to-pdf` or `get-mail-attachment` with the attachment id to fetch._
64
94
  ```
65
95
 
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:
96
+ Those two bracketed lines are the entire cost of a 40-message thread and a signature logo. Attachments arrive with their ids ready for the follow-up call:
67
97
 
68
98
  ```console
69
99
  $ ask-marcel-office convert-mail-attachment-to-markdown \
@@ -77,22 +107,11 @@ $ ask-marcel-office microsoft-search-query --query "Q3 budget filetype:pptx"
77
107
  $ ask-marcel-office download-drive-item-as-markdown --drive-id "b!abc..." --item-id "01BYE..."
78
108
  ```
79
109
 
80
- ## The difference in practice
81
-
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
110
  ## What your agent can reach
92
111
 
93
112
  | Surface | Commands | In practice |
94
113
  |---|---:|---|
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) |
114
+ | 📧 Outlook Mail | 44 | 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
115
  | 📁 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
116
  | 📅 Calendar | 24 | "What's on this week" via relative dates (`today`, `start-of-week`, `+7d`), event details, free/busy lookups |
98
117
  | 👥 People + directory | 16 | People search, user profiles, the directory around you, your own identity and IDs in one round trip |
@@ -178,13 +197,13 @@ claude mcp add --transport stdio --scope user ask-marcel-office -- npx -y ask-ma
178
197
 
179
198
  > **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`.
180
199
 
181
- You get **five gateway tools**, not one per command — 184 tool schemas would bloat every session, the opposite of the point:
200
+ You get **five gateway tools**, not one per command — a schema per command would bloat every session, the opposite of the point:
182
201
 
183
202
  | Tool | Does |
184
203
  |:--|:--|
185
204
  | `list-commands` | The terse manifest. Start here; `category` narrows it. |
186
205
  | `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. |
206
+ | `run-command` | The 188 **read** commands. `readOnlyHint: true`, so clients can auto-approve it. |
188
207
  | `run-write-command` | The 4 mail-draft **write** commands. Separate tool so the read tool's promise stays honest. |
189
208
  | `login` | Sign in / refresh. Opens a browser on this machine. |
190
209
 
@@ -230,14 +249,14 @@ Azure Managed Identity, an on-behalf-of flow, hand-pasted JWTs in tests: the Gra
230
249
 
231
250
  ## Deep docs
232
251
 
233
- - **[All 184 commands](docs/COMMANDS.md)**: per-category tables with required params + Graph endpoint
252
+ - **[All 192 commands](docs/COMMANDS.md)**: per-category tables with required params + Graph endpoint
234
253
  - **[Usage guide](docs/USAGE.md)**: output formats, OData passthrough, `--output-path`, pagination, library API, architecture, configuration
235
254
  - **[Machine-readable manifest](docs/commands.json)**: JSON for programmatic discovery, also importable via `import manifest from 'ask-marcel-office-cli/commands.json'`
236
255
  - **[QA playbook](docs/QA-PLAYBOOK.md)**: the repeatable full-surface health check run before each release
237
256
 
238
257
  ## Roadmap
239
258
 
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).
259
+ 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/ask-marcel/ask-marcel-office-cli/issues).
241
260
 
242
261
  ## Built with
243
262