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.
- package/CHANGELOG.md +203 -0
- package/README.md +178 -119
- package/dist/cli.js +44806 -27142
- package/dist/commands.json +371 -52
- package/dist/composition/build-deps.d.ts +8 -0
- package/dist/composition/mcp.d.ts +20 -0
- package/dist/composition/run-registry-command.d.ts +34 -0
- package/dist/domain/tenant-id.d.ts +9 -0
- package/dist/domain/utilities/spo-tenant.d.ts +20 -0
- package/dist/index.js +2735 -1674
- package/dist/infra/auth.d.ts +36 -14
- package/dist/infra/browser-auth.d.ts +7 -2
- package/dist/infra/graph-client.d.ts +52 -16
- package/dist/presenter/graph-cursor.d.ts +2 -0
- package/dist/presenter/output.d.ts +1 -1
- package/dist/presenter/render-to-string.d.ts +15 -0
- package/dist/use-cases/commands/build-command.d.ts +19 -2
- package/dist/use-cases/commands/command-types.d.ts +16 -11
- package/dist/use-cases/commands/convert-calendar-event-attachment-to-markdown.d.ts +4 -0
- package/dist/use-cases/commands/{convert-drive-item-zip.d.ts → convert-drive-item-zip-to-markdown.d.ts} +5 -0
- package/dist/use-cases/commands/{convert-local-file.d.ts → convert-local-file-to-markdown.d.ts} +4 -0
- package/dist/use-cases/commands/convert-mail-attachment-to-markdown.d.ts +10 -2
- package/dist/use-cases/commands/{convert-mail-attachment-zip.d.ts → convert-mail-attachment-zip-to-markdown.d.ts} +5 -1
- package/dist/use-cases/commands/convert-mail-to-markdown.d.ts +35 -1
- package/dist/use-cases/commands/create-forward-draft.d.ts +4 -0
- package/dist/use-cases/commands/create-reply-draft.d.ts +8 -0
- package/dist/use-cases/commands/download-drive-item-as-markdown.d.ts +7 -2
- package/dist/use-cases/commands/download-drive-item-as-pdf.d.ts +1 -0
- package/dist/use-cases/commands/download-drive-item-content.d.ts +1 -0
- package/dist/use-cases/commands/draft-comment-splicer.d.ts +29 -0
- package/dist/use-cases/commands/draft-dedup.d.ts +18 -0
- package/dist/use-cases/commands/draft-response.d.ts +4 -0
- package/dist/use-cases/commands/extract-drive-item-images.d.ts +1 -0
- package/dist/use-cases/commands/extract-local-file-images.d.ts +1 -1
- package/dist/use-cases/commands/fetch-raw-bytes.d.ts +13 -0
- package/dist/use-cases/commands/find-mail-drafts.d.ts +9 -0
- package/dist/use-cases/commands/get-mail-signature.d.ts +8 -0
- package/dist/use-cases/commands/get-user.d.ts +10 -0
- package/dist/use-cases/commands/inline-image-embedder.d.ts +2 -1
- package/dist/use-cases/commands/login-status.d.ts +9 -27
- package/dist/use-cases/commands/login.d.ts +21 -0
- package/dist/use-cases/commands/mail-message-select.d.ts +1 -0
- package/dist/use-cases/commands/mail-quote-stripper.d.ts +16 -15
- package/dist/use-cases/commands/markdown-dispatch.d.ts +2 -1
- package/dist/use-cases/commands/msg-to-markdown.d.ts +10 -1
- package/dist/use-cases/commands/odata-query.d.ts +15 -1
- package/dist/use-cases/commands/office-to-markdown.d.ts +1 -0
- package/dist/use-cases/commands/read-mail-attachment.d.ts +4 -0
- package/dist/use-cases/commands/resolve-command.d.ts +25 -0
- package/dist/use-cases/commands/search-all-files.d.ts +8 -0
- package/dist/use-cases/commands/search-escape.d.ts +23 -0
- package/dist/use-cases/commands/signature-extractor.d.ts +2 -0
- package/dist/use-cases/commands/tenant-option.d.ts +44 -0
- package/dist/use-cases/commands/update-mail-draft.d.ts +1 -0
- package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +10 -5
- package/dist/use-cases/ports/filesystem.d.ts +1 -1
- package/docs/COMMANDS.md +34 -29
- package/docs/USAGE.md +65 -7
- 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
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# ask-marcel-office
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
9
|
+
[](https://www.npmjs.com/package/ask-marcel-office-cli)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](https://nodejs.org)
|
|
12
|
+
[](https://bun.sh)
|
|
13
|
+
[](docs/USAGE.md)
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
Outlook · OneDrive · SharePoint · Calendar · Excel · Teams · Planner · To Do · OneNote · People
|
|
17
16
|
|
|
18
|
-
|
|
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
|
-
|
|
19
|
+
</div>
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
---
|
|
25
22
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
## The three walls it removes
|
|
35
36
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
### 🛡️ Safe to hand to an autonomous agent
|
|
45
42
|
|
|
46
|
-
|
|
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
|
-
|
|
45
|
+
### 🧠 Responses budgeted for a context window
|
|
56
46
|
|
|
57
|
-
|
|
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
|
-
|
|
49
|
+
## See it work
|
|
60
50
|
|
|
61
|
-
|
|
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
|
-
|
|
53
|
+
```console
|
|
54
|
+
$ ask-marcel-office convert-mail-to-markdown --message-id "AAMkAD..."
|
|
64
55
|
|
|
65
|
-
|
|
56
|
+
# Q3 budget review: action needed
|
|
57
|
+
**From:** Robin Chen <robin.chen@contoso.com>
|
|
66
58
|
|
|
67
|
-
|
|
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
|
-
|
|
62
|
+
## Attachments
|
|
63
|
+
- Q3-forecast.xlsx (48 KB) · id AAMkAD...
|
|
64
|
+
```
|
|
70
65
|
|
|
71
|
-
|
|
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
|
-
```
|
|
74
|
-
ask-marcel-office
|
|
75
|
-
|
|
68
|
+
```console
|
|
69
|
+
$ ask-marcel-office convert-mail-attachment-to-markdown \
|
|
70
|
+
--message-id "AAMkAD..." --attachment-id "AAMkAD..."
|
|
76
71
|
```
|
|
77
72
|
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
## The difference in practice
|
|
85
81
|
|
|
86
|
-
|
|
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
|
-
|
|
136
|
+
```bash
|
|
137
|
+
# Bun >=1.0 or Node >=20 · macOS, Windows, Linux
|
|
138
|
+
npm i -g ask-marcel-office-cli
|
|
89
139
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
+
**Claude Code** — one command:
|
|
100
153
|
|
|
101
154
|
```bash
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
+
## Embed it as a TypeScript library
|
|
135
194
|
|
|
136
|
-
|
|
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
|
|
203
|
+
// result.value is the Graph payload
|
|
147
204
|
}
|
|
148
205
|
```
|
|
149
206
|
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
181
|
-
- **[Usage guide](docs/USAGE.md)
|
|
182
|
-
- **[Machine-readable manifest](docs/commands.json)
|
|
183
|
-
- **[QA playbook](docs/QA-PLAYBOOK.md)
|
|
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.
|
|
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
|
|
194
|
-
- **Microsoft Graph v1.0
|
|
195
|
-
- **Playwright
|
|
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>
|