ask-marcel-office-cli 2.1.0 → 2.3.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 (63) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/README.md +178 -119
  3. package/dist/cli.js +44725 -27155
  4. package/dist/commands.json +406 -734
  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 +43 -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 +2831 -1833
  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 +2 -2
  16. package/dist/presenter/render-to-string.d.ts +47 -0
  17. package/dist/use-cases/commands/build-command.d.ts +19 -2
  18. package/dist/use-cases/commands/command-types.d.ts +17 -31
  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 +5 -1
  26. package/dist/use-cases/commands/create-reply-draft.d.ts +9 -1
  27. package/dist/use-cases/commands/docs-render.d.ts +0 -1
  28. package/dist/use-cases/commands/download-drive-item-as-markdown.d.ts +7 -2
  29. package/dist/use-cases/commands/download-drive-item-as-pdf.d.ts +1 -0
  30. package/dist/use-cases/commands/download-drive-item-content.d.ts +1 -0
  31. package/dist/use-cases/commands/draft-comment-splicer.d.ts +36 -0
  32. package/dist/use-cases/commands/draft-dedup.d.ts +18 -0
  33. package/dist/use-cases/commands/draft-response.d.ts +4 -0
  34. package/dist/use-cases/commands/extract-drive-item-images.d.ts +1 -0
  35. package/dist/use-cases/commands/extract-local-file-images.d.ts +1 -1
  36. package/dist/use-cases/commands/fetch-raw-bytes.d.ts +13 -0
  37. package/dist/use-cases/commands/find-mail-drafts.d.ts +9 -0
  38. package/dist/use-cases/commands/get-mail-signature.d.ts +8 -0
  39. package/dist/use-cases/commands/get-user.d.ts +10 -0
  40. package/dist/use-cases/commands/inline-image-embedder.d.ts +2 -1
  41. package/dist/use-cases/commands/list-mail-folder-messages-delta.d.ts +10 -2
  42. package/dist/use-cases/commands/login-status.d.ts +9 -27
  43. package/dist/use-cases/commands/login.d.ts +21 -0
  44. package/dist/use-cases/commands/mail-message-select.d.ts +1 -0
  45. package/dist/use-cases/commands/mail-quote-stripper.d.ts +16 -15
  46. package/dist/use-cases/commands/markdown-dispatch.d.ts +2 -1
  47. package/dist/use-cases/commands/msg-to-markdown.d.ts +10 -1
  48. package/dist/use-cases/commands/odata-query.d.ts +15 -1
  49. package/dist/use-cases/commands/office-to-markdown.d.ts +1 -0
  50. package/dist/use-cases/commands/read-mail-attachment.d.ts +4 -0
  51. package/dist/use-cases/commands/reject-unknown-params.d.ts +16 -0
  52. package/dist/use-cases/commands/resolve-command.d.ts +23 -0
  53. package/dist/use-cases/commands/search-all-files.d.ts +8 -0
  54. package/dist/use-cases/commands/search-escape.d.ts +23 -0
  55. package/dist/use-cases/commands/search-onenote-pages.d.ts +1 -1
  56. package/dist/use-cases/commands/signature-extractor.d.ts +2 -0
  57. package/dist/use-cases/commands/tenant-option.d.ts +44 -0
  58. package/dist/use-cases/commands/update-mail-draft.d.ts +5 -0
  59. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +10 -5
  60. package/dist/use-cases/ports/filesystem.d.ts +1 -1
  61. package/docs/COMMANDS.md +38 -33
  62. package/docs/USAGE.md +65 -7
  63. package/package.json +2 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,289 @@
2
2
 
3
3
  All notable changes to `ask-marcel-office-cli` are documented here.
4
4
 
5
+ ## 2.3.0
6
+
7
+ > **Read this before upgrading.** This release contains breaking changes to the
8
+ > flag surface despite the minor version number. If you pin `^2.2.0` you will
9
+ > receive it automatically. Anything calling the CLI, the MCP gateway, or the
10
+ > library with an old flag spelling will stop working.
11
+
12
+ ### Removed — every flag alias and every deprecated command name
13
+
14
+ One name per flag, one name per command. The alias system is gone: 77 flag
15
+ aliases and 4 deprecated command names. Each alias already pointed at a more
16
+ specific canonical, so nothing was renamed to accommodate the removal.
17
+
18
+ | Gone | Use instead |
19
+ |------|-------------|
20
+ | `--id` (51 commands) | the command's specific id flag (`--message-id`, `--site-id`, …) |
21
+ | `--start` / `--end` (7 date-window commands) | `--start-date-time` / `--end-date-time` |
22
+ | `--body-content` on `create-reply-draft` / `create-forward-draft` | `--comment` |
23
+ | `--task-id` | `--planner-task-id` or `--todo-task-id` |
24
+ | `--folder-id` | `--item-id` |
25
+ | `--item-id` on the SharePoint list commands | `--list-item-id` |
26
+ | `--page-id`, `--section-id` | `--onenote-page-id`, `--onenote-section-id` |
27
+ | `--emails` on `get-schedule` | `--schedules` |
28
+ | `--query` on `search-onenote-pages` | *(now the canonical name, see below)* |
29
+ | `download-onedrive-file-content` | `download-drive-item-content` |
30
+ | `convert-local-file` | `convert-local-file-to-markdown` |
31
+ | `convert-drive-item-zip` | `convert-drive-item-zip-to-markdown` |
32
+ | `convert-mail-attachment-zip` | `convert-mail-attachment-zip-to-markdown` |
33
+
34
+ Why: the same spelling meant different things on sibling commands. That is how
35
+ `--body-content` came to mean "the text above the quote" on the create-draft
36
+ commands and "replace the ENTIRE body, quote included" on `update-mail-draft`,
37
+ a silent data-loss trap fixed in this same release.
38
+
39
+ ### Changed — `search-onenote-pages --title-substring` is now `--query`
40
+
41
+ The last flag naming a shared concept differently from its siblings. All eight
42
+ search commands now take `--query`. (`--query` had already been bolted onto this
43
+ command as an alias for exactly this reason; it is now the only name.) The flag
44
+ still matches page TITLES only, which its description states explicitly.
45
+
46
+ ### Changed — a parameter a command does not declare is now refused
47
+
48
+ Previously the three surfaces disagreed. Commander rejected unknown `--flags` on
49
+ the CLI, but Zod strips unknown keys, so the MCP gateway's `run-command` params
50
+ and direct library `commands[x].execute(...)` calls silently swallowed them and
51
+ returned data that looked like it had obeyed. A live audit found this on 5 of 7
52
+ delta commands. All three surfaces now refuse identically, with
53
+ `errorCode: "unknown_parameter"` and the supported flags named in the message.
54
+
55
+ ### Fixed — `--top` no longer terminates a mail-folder delta sync
56
+
57
+ `list-mail-folder-messages-delta --top 2` on a 67-message Inbox returned 2
58
+ messages and an `@odata.deltaLink` rather than a `nextLink`. Graph reads a
59
+ *satisfied* `$top` as "this sync is complete", so the other 65 messages were
60
+ never delivered and the caller banked a delta token certifying a sync that never
61
+ happened. `--top` is now sent as `Prefer: odata.maxpagesize`, which pages
62
+ normally. `--skip` and `--orderby` are no longer advertised there (Graph ignores
63
+ the first and rejects the second), and `--filter` / `--orderby` are gone from
64
+ the two drive delta commands for the same reason.
65
+
66
+ ### Fixed — a whole-body draft update can no longer drop quoted history
67
+
68
+ `update-mail-draft --body-content` on a threaded draft replaced the entire body
69
+ including the quoted thread, with nothing warning at call time. It now reads the
70
+ draft first and refuses when a quote is present, pointing at `--comment`. Pass
71
+ `--replace-quoted-history true` for the deliberate case. The same read refuses
72
+ non-drafts before the write.
73
+
74
+ ### Fixed — the oversized-response banner only names remedies that exist
75
+
76
+ The `sizeHint` banner called `--output-path` a "universal remedy (works on every
77
+ command)". It is not: plain-JSON commands refuse the flag, and the two commands
78
+ that trip the banner most often (`search-all-files`, `microsoft-search-query`)
79
+ advertise only `--query`, so every remedy it named was a dead end. The banner is
80
+ now derived from the command that produced the payload and the surface asking:
81
+ byte-producing commands keep the `--output-path` wording (`outputPath` on MCP),
82
+ plain-JSON commands on the CLI get a shell redirect, and the same commands on
83
+ MCP get neither, since an MCP client has no shell.
84
+
85
+ ## 2.2.0
86
+
87
+ ### Added
88
+
89
+ - **`ask-marcel-office mcp` — serve the whole CLI to any MCP client over stdio.** Hosts
90
+ without a shell (Claude Desktop, other MCP clients) can now reach every
91
+ command. Register it with:
92
+
93
+ ```bash
94
+ claude mcp add --transport stdio --scope user ask-marcel-office -- ask-marcel-office mcp
95
+ ```
96
+
97
+ It exposes **five gateway tools**, not one per command — 183 tool schemas
98
+ would inject hundreds of KB into every session, the exact token bloat this
99
+ CLI exists to avoid:
100
+
101
+ - `list-commands` — the terse manifest (`{name, summary, category}`),
102
+ optionally filtered to one category. The discovery entry point.
103
+ - `get-command-docs` — full Markdown docs for a single command: every option,
104
+ its Graph endpoint, an example, the response shape. Lifecycle commands and
105
+ deprecated command names both resolve.
106
+ - `run-command` — runs any of the **179 read** commands. Declared
107
+ `readOnlyHint: true`, so an MCP client can auto-approve it; a write routed
108
+ here is refused *before* it executes.
109
+ - `run-write-command` — runs the **4** mail-draft write commands
110
+ (`create-mail-draft`, `create-forward-draft`, `create-reply-draft`,
111
+ `update-mail-draft`). A separate tool precisely so the 179 read commands
112
+ keep an honest read-only annotation. Marked non-destructive: every write
113
+ produces an UNSENT draft, and this CLI still cannot send mail.
114
+ - `login` — sign in or refresh. The elevated (M365) token lapses roughly
115
+ hourly and only a browser can recapture it, so this saves a terminal
116
+ round-trip mid-session.
117
+
118
+ Both run tools accept `outputPath` / `outputDir`, mirroring the CLI flags, so
119
+ a multi-MB PDF lands on disk instead of flooding the model's context.
120
+
121
+ **Raise your MCP client's tool timeout to ~5 minutes** (`MCP_TOOL_TIMEOUT=300000`
122
+ or equivalent). Measured against a real tenant: a browser sign-in takes
123
+ **37–64 s even with no MFA prompt**, while the MCP default request timeout is
124
+ **60 s** — so `login` times out intermittently at the default. The server keeps
125
+ running through a client-side timeout, so the sign-in has usually completed
126
+ anyway; re-run your original command before calling `login` again. Sign in
127
+ from a terminal the first time (`ask-marcel-office login`), where an MFA prompt adds
128
+ minutes on top.
129
+
130
+ Read/write sets are derived from the registry's `mutates` flag, so a new
131
+ command lands on the correct tool with no code change here.
132
+
133
+ - **Files in a partner tenant you are a guest in are now readable.** Previously
134
+ every such read died at `401 invalidAudienceUri: Invalid audience Uri
135
+ '00000003-0000-0ff1-ce00-000000000000'` (SharePoint Online's app id): your
136
+ home-tenant Graph cannot mint a SharePoint token for a foreign tenant, so no
137
+ home-tier token could reach the file — which is exactly the case when a partner
138
+ sends you a "Copy link" URL.
139
+
140
+ **`resolve-drive-share-link` crosses the boundary by itself.** It tries your
141
+ home token and, only on that specific error, identifies the owning tenant from
142
+ the URL host (`contoso.sharepoint.com` → `contoso.onmicrosoft.com` → the
143
+ tenant's public OIDC discovery document) and retries with a guest token minted
144
+ by redeeming your existing refresh token against that tenant's authority. No
145
+ new sign-in, no browser, no Azure app. It returns the tenant as **`tenantId`**;
146
+ the field's PRESENCE is the signal — absent means the file is in your own
147
+ tenant and nothing changes.
148
+
149
+ **`--tenant-id` carries it to the rest of the family.** `driveId` and `itemId`
150
+ carry no tenant, so the commands that consume them cannot recover on their own;
151
+ pass the `tenantId` that `resolve-drive-share-link` returned and they sign with
152
+ the guest token instead. Available on `get-drive-item`,
153
+ `download-drive-item-content`, `download-drive-item-as-markdown`,
154
+ `download-drive-item-as-pdf`, `extract-drive-item-images`,
155
+ `convert-drive-item-zip`, `get-drive-item-list-item`, `list-folder-files`,
156
+ `list-drive-item-versions`, `list-drive-item-permissions`, and
157
+ `list-drive-item-thumbnails`. Optional everywhere; omit it for your own tenant.
158
+
159
+ Boundaries worth knowing: a link you simply may not read still fails with
160
+ `accessDenied` (a guest token would not help, so one is never requested); a
161
+ tenant that has not consented to this client, or where you are not actually a
162
+ guest, fails with a message naming that tenant; a SharePoint host whose sign-in
163
+ domain differs from its name cannot be resolved and says so (`--tenant-id`
164
+ passed by hand still works there); and `1drv.ms` belongs to no tenant at all,
165
+ so it is unaffected. Elevated commands do not take the flag — the elevated
166
+ token is a home-tenant identity, so "elevated in a partner tenant" does not
167
+ exist. Verified end to end against a real partner tenant.
168
+
169
+ - **`get-user`** looks up a directory user by id, UPN/email, or name — one
170
+ command, two routes. An **Azure AD id, UPN, or email** returns that user's FULL
171
+ profile via `GET /users/{id}` on the elevated M365 token (`User.Read.All`, so
172
+ `jobTitle` / `department` / `officeLocation` / phones are populated); it honours
173
+ `--select` / `--expand` and fail-fasts with `secondary_token_unavailable` when
174
+ the elevated token is cold (run `login --force`). An email that is the user's
175
+ `mail` but not their sign-in UPN — every **guest / B2B** user, whose UPN is the
176
+ `alias_homeorg#EXT#@tenant` form — still resolves: when the direct
177
+ `GET /users/{id}` 404s, the command falls back to
178
+ `GET /users?$filter=mail eq '<email>'` and returns the single match. A bare **name** instead
179
+ searches the signed-in user's relevant-people graph
180
+ (`GET /me/people?$search="name"`) on the basic token — so it works even when the
181
+ elevated token is cold — and returns candidate matches
182
+ (`{ id, displayName, mail, jobTitle, department }`) so the caller can
183
+ disambiguate and re-query by the chosen `id` for the full card. Name search
184
+ covers your people graph, not the whole tenant; `microsoft-search-query` remains
185
+ the broad tenant-wide person search. (181st command.)
186
+
187
+ ### Changed
188
+
189
+ - **Command execution is now shared by both front ends.** The per-command
190
+ handler (alias normalization, local-filesystem routing, error-source
191
+ classification, `--output-path` / `--output-dir` persistence) moved out of
192
+ `cli.ts` into `composition/run-registry-command.ts`, which the CLI and the MCP
193
+ gateway both call. Behaviour is unchanged — the existing CLI suite passes
194
+ untouched — but a future fix now reaches both surfaces instead of one.
195
+ - **The output envelope logic is now pure and reusable.** `presenter/output.ts`
196
+ wrote directly to stdout, which an MCP stdio server cannot do (stdout is its
197
+ JSON-RPC channel). The envelope layer moved to `presenter/render-to-string.ts`
198
+ (`renderToString` / `renderErrorToString`); `output.ts` is a thin stdout shim
199
+ over it. Output is byte-identical. MCP callers therefore get the same
200
+ `hint:` / `source:` remedies the terminal does.
201
+
202
+ - **`scopes-check` is now the single detailed token-status view.** Per token
203
+ (basic / elevated / chatsvcagg / ic3) it reports the `available` flag,
204
+ seconds-to-expiry, `refresh` route (`automatic` = self-heals from the shared
205
+ refresh token; `interactive` = the elevated token, needs a browser login), and
206
+ — new — that token's OWN granted scopes, decoded from its `scp` claim. The four
207
+ tokens carry distinct scope sets (basic ~31 Graph scopes, elevated ~20,
208
+ chatsvcagg `user_impersonation`, ic3 `Teams.AccessAsUser.All`), so an agent can
209
+ intersect each tier against a command's `scopesRequired`. Additive and
210
+ non-breaking: the flat top-level `scopes`/`audience`/`expiresAt`/`expiresInSeconds`
211
+ (the basic token) are unchanged. A `hint` field names a forced re-login as the
212
+ single refresh action.
213
+ - **`login` slimmed to an auth confirmation.** It now prints
214
+ `{ status: "authenticated", available: [...], hint }` — which token tiers are
215
+ available, plus a pointer to `scopes-check` (per-token detail) and `login --force`
216
+ (refresh) — instead of the four-token detail block from 2.1.0, and the confusing
217
+ refresh-mechanism hint is gone. The detailed per-token status now lives only in
218
+ `scopes-check`. (Dropping login's `tokens` block is the one breaking-ish change;
219
+ it shipped a single release earlier and the same detail is fully available via
220
+ `scopes-check`.)
221
+ - **The mail read commands now include `conversationId` in their default
222
+ projection.** `list-mail-messages`, `search-mail-messages`, and
223
+ `get-mail-message` add `conversationId` to their slim default `--select`, so a
224
+ caller can group results into a thread — or hand the id straight to
225
+ `list-conversation-messages` — without a second round-trip. The three
226
+ previously-duplicated default-select strings are now one shared constant
227
+ (`mail-message-select.ts`) so they cannot drift apart again. Additive
228
+ (~76 bytes/message); a user-supplied `--select` still overrides entirely.
229
+ - **`resolve-drive-share-link` now resolves a sharing URL to the driveItem in one
230
+ call.** It previously only encoded the URL into the `u!` share token (offline,
231
+ no Graph call), forcing a second `/shares/{token}/driveItem` fetch plus
232
+ hand-parsing `parentReference.driveId`. It now encodes AND fetches, returning
233
+ `{ driveId, itemId, name, webUrl, size, lastModifiedDateTime, shareToken }` — the
234
+ two ids every `*-drive-item` command needs, in one shot (basic token,
235
+ `Files.Read.All`). Shape + behavior change: it now makes a Graph call and no
236
+ longer returns `graphPath`/`originalUrl`; a cross-tenant or no-access link
237
+ surfaces the Graph `accessDenied` / `itemNotFound` instead of a share token that
238
+ would only fail on the follow-up call.
239
+
240
+ ### Fixed
241
+
242
+ - **`login` no longer sends you round a loop it cannot break.** A command needing
243
+ the elevated (M365) token failed with "run `ask-marcel-office login`"; `login`
244
+ answered `authenticated` and changed nothing; the command failed identically.
245
+ Forever. The elevated token carries no refresh token of its own, so a plain
246
+ `login` that found a valid cached basic token returned on the cache rung
247
+ without ever re-capturing it — `--force` was the only escape, and the error
248
+ message never said so.
249
+
250
+ `login` now re-captures the elevated token whenever it is missing, and does
251
+ nothing when it is already present (no gratuitous browser). The fail-fast
252
+ message now names the right remedy per tier and explains why: `--force` for
253
+ elevated, plain `login` for the chatsvcagg / ic3 substrate tokens, which
254
+ self-heal from the shared refresh token and never needed a browser at all.
255
+ `scopes-check`'s hint and the README carried the same false "only way" claim and
256
+ are corrected too. `--force` still works and still re-captures every tier.
257
+
258
+ - **The library's documented `AuthManager` example did not compile.** The README's
259
+ "bring your own token" snippet passed `{getAccessToken, logout}` to
260
+ `createGraphClient` and called `AuthManager` "two async methods"; it has ten.
261
+ The snippet now compiles, and declines the tiers a custom token source cannot
262
+ mint rather than omitting them.
263
+
264
+ - **Guest / external-user (B2B) lookups by UPN now resolve.** A guest UPN is
265
+ `alice_contoso.com#EXT#@fabrikam.onmicrosoft.com`, and the `#` is the URL
266
+ fragment delimiter — the seven commands that put a caller-supplied `userId`
267
+ in a `/users/{id}` path (`get-user-manager`, `get-shared-mailbox-message`,
268
+ `list-shared-mailbox-messages`, `list-shared-mailbox-folder-messages`,
269
+ `list-shared-calendar-events`, `list-user-direct-reports`,
270
+ `list-shared-calendar-view`) interpolated it raw, so `fetch` dropped everything
271
+ from the `#` onward and queried the wrong user. The `userId` segment is now
272
+ percent-encoded (`#`→`%23`, `@`→`%40`; GUIDs unchanged), which Microsoft Graph
273
+ requires for B2B UPNs and which is verified live against the directory. `get-user`
274
+ (new this release) already encodes its path.
275
+ - **Cached elevated / substrate tokens now survive a basic-token refresh.** The
276
+ silent basic-token refresh (triggered whenever the Teams token nears expiry)
277
+ rebuilt the cache from only the three basic fields, wiping the cached `elevated`
278
+ / `chatsvcagg` / `ic3` tokens on every renewal — so `scopes-check` and `login`
279
+ then reported them unavailable with empty scopes, and the elevated token (which
280
+ carries no refresh token of its own) needed a forced re-login to recover. The
281
+ refresh now merges, preserving all four tokens until they each expire.
282
+ - **`scopes-check` explains an unavailable token.** Every tier block whose
283
+ `available` is `false` now carries a `reason` — a one-line note on why it is
284
+ absent and how to restore it (`login --force`; the substrate tiers also self-heal
285
+ on next use) — so an empty `scopes: []` on a missing token is not misread as "no
286
+ scopes". Additive and non-breaking; omitted when the token is available.
287
+
5
288
  ## 2.1.0
6
289
 
7
290
  ### Added