ask-marcel-office-cli 2.0.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 (61) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +178 -118
  3. package/dist/cli.js +44841 -26996
  4. package/dist/commands.json +423 -51
  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 +2671 -1463
  11. package/dist/infra/auth.d.ts +55 -3
  12. package/dist/infra/browser-auth.d.ts +10 -3
  13. package/dist/infra/graph-client.d.ts +62 -0
  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} +8 -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 +16 -0
  26. package/dist/use-cases/commands/create-reply-draft.d.ts +5 -1
  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/image-extraction.d.ts +2 -1
  40. package/dist/use-cases/commands/inline-image-embedder.d.ts +2 -1
  41. package/dist/use-cases/commands/login-status.d.ts +13 -0
  42. package/dist/use-cases/commands/login.d.ts +24 -1
  43. package/dist/use-cases/commands/mail-message-select.d.ts +1 -0
  44. package/dist/use-cases/commands/mail-quote-stripper.d.ts +16 -15
  45. package/dist/use-cases/commands/markdown-dispatch.d.ts +2 -1
  46. package/dist/use-cases/commands/msg-to-markdown.d.ts +10 -1
  47. package/dist/use-cases/commands/odata-query.d.ts +15 -1
  48. package/dist/use-cases/commands/office-to-markdown.d.ts +1 -0
  49. package/dist/use-cases/commands/parse-recipients.d.ts +14 -0
  50. package/dist/use-cases/commands/read-mail-attachment.d.ts +4 -0
  51. package/dist/use-cases/commands/resolve-command.d.ts +25 -0
  52. package/dist/use-cases/commands/search-all-files.d.ts +8 -0
  53. package/dist/use-cases/commands/search-escape.d.ts +23 -0
  54. package/dist/use-cases/commands/signature-extractor.d.ts +2 -0
  55. package/dist/use-cases/commands/tenant-option.d.ts +44 -0
  56. package/dist/use-cases/commands/update-mail-draft.d.ts +1 -0
  57. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +12 -5
  58. package/dist/use-cases/ports/filesystem.d.ts +1 -1
  59. package/docs/COMMANDS.md +34 -28
  60. package/docs/USAGE.md +65 -7
  61. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,275 @@
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
+
208
+ ## 2.1.0
209
+
210
+ ### Added
211
+
212
+ - **`create-forward-draft`** creates an UNSENT forward draft of an existing
213
+ message. `POST /me/messages/{id}/createForward` mints the draft (`FW:` subject,
214
+ quoted original) with your comment placed above the quote and the recipients
215
+ set, in one call (`Mail.ReadWrite`, already on the basic token).
216
+ `--to-recipients` is required (a forward with no recipient is not actionable);
217
+ `--cc-recipients` and a `--subject` override are optional. Like the other
218
+ mail-draft commands, it produces an UNSENT draft only; the CLI can never send.
219
+ This is the fourth and last write command, closing the "forward to the right
220
+ owner" gap that `create-reply-draft` (in-thread) could not.
221
+ - **`convert-local-file --include-images`** (a `.zip` only) also extracts every
222
+ archive entry's embedded images (docx/xlsx/pptx OOXML media parts, pdf page
223
+ images), so a screenshot pasted inside a zipped document is reachable in one
224
+ call.
225
+ - **`login` now reports all four cached tokens** (basic, elevated/M365, and the
226
+ two Teams-chat substrate tokens chatsvcagg / ic3) with each one's time-left and
227
+ refresh route, so running `login` while already signed in shows the full token
228
+ picture instead of a bare `{ status: "authenticated" }`. Each token is
229
+ `{ available, expiresInSeconds?, refresh: "automatic" | "interactive", reason? }`:
230
+ basic/chatsvcagg/ic3 refresh automatically from the cached refresh token; the
231
+ elevated token is `interactive` (re-captured only on a browser login).
232
+ - **`login --force`** ignores the cache and re-captures every token via the
233
+ browser in one pass — the only way to refresh the elevated token while the
234
+ basic token is still valid. The persistent browser profile is reused, so you
235
+ are usually not re-prompted for credentials.
236
+ - **`scopes-check` now reports the elevated (M365ChatClient) token** plus the two
237
+ Teams-chat substrate tokens (`chatsvcagg` / `ic3`), each in an
238
+ `{ available, expiresInSeconds? }` block, so a fresh process can pre-flight the
239
+ historical-version download / convert commands instead of discovering a 403
240
+ mid-run. The two substrate blocks are additive; the existing top-level fields
241
+ are unchanged.
242
+ - **Machine-readable `errorCode`s on more error paths** — the elevated /
243
+ substrate fail-fast (`secondary_token_unavailable`) and the client-side
244
+ unsupported-input rejections (`unsupported_image` / `unsupported_format` /
245
+ `unsupported_legacy_office` / `unsupported_document`), so an agent branches on
246
+ a stable code instead of substring-matching the message.
247
+
248
+ ### Fixed
249
+
250
+ - **`create-forward-draft` and `create-reply-draft` no longer drop the forwarded
251
+ / quoted body.** They set the comment via Graph's `comment` parameter on the
252
+ `createForward` / `createReplyAll` POST, which places it above the preserved
253
+ quote. The previous implementation PATCHed `body` with only the comment, which
254
+ **replaced** the whole draft body and dropped the entire forwarded original (a
255
+ forward went out with just the comment, no message). Caught by a live smoke
256
+ test; the fix is live-verified.
257
+
258
+ ### Changed
259
+
260
+ - The `parseRecipients` helper shared by the mail-draft write commands moved to
261
+ `parse-recipients.ts` (one definition, three call sites), with no behaviour
262
+ change.
263
+ - `scopes-check` `responseShape` corrected: `elevated.expiresInSeconds` is
264
+ omitted (the key is absent) when no elevated token is cached, not `null`.
265
+
266
+ ### Removed
267
+
268
+ - The `--body-content-type` flag on **`create-forward-draft` and
269
+ `create-reply-draft`** is removed. It never affected the quoted body (Graph
270
+ embeds the comment / reply as text above the quote), so it was a no-op on those
271
+ two commands. It remains on `create-mail-draft` and `update-mail-draft`, which
272
+ set the body directly.
273
+
5
274
  ## 2.0.0
6
275
 
7
276
  Breaking auth simplification, a repo-wide privacy scrub (including a rewrite of