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/README.md CHANGED
@@ -1,140 +1,198 @@
1
- # ask-marcel-office-cli
1
+ <div align="center">
2
2
 
3
- **A Microsoft Graph CLI built for LLMs.** 179 commands across Mail, Calendar, OneDrive, SharePoint, Excel, Teams chats, Planner / To-Do, OneNote, and directory — plus local-file tools (markdown conversion, image extraction) that need no sign-in at all. Sign in once with your Microsoft 365 account — no Azure app registration, no admin consent, no client secrets.
3
+ # ask-marcel-office
4
4
 
5
- ```bash
6
- npm i -g ask-marcel-office-cli
7
- ask-marcel-office login # browser opens once, token cached
8
- ask-marcel-office my-quick-context # who am I + my IDs, in one round trip
9
- ask-marcel-office list-mail-messages --top 5
10
- ```
5
+ ### The Microsoft 365 command line built for AI agents
11
6
 
12
- ---
7
+ **One sign-in. 180 read-only commands. Every document as clean markdown.**
13
8
 
14
- ## Why it exists
9
+ [![npm version](https://img.shields.io/npm/v/ask-marcel-office-cli.svg?logo=npm&color=cb3837)](https://www.npmjs.com/package/ask-marcel-office-cli)
10
+ [![license: MIT](https://img.shields.io/npm/l/ask-marcel-office-cli.svg?color=blue)](LICENSE)
11
+ [![node ≥20](https://img.shields.io/node/v/ask-marcel-office-cli?logo=node.js&color=339933)](https://nodejs.org)
12
+ [![bun ≥1.0](https://img.shields.io/badge/bun-%E2%89%A51.0-14151a?logo=bun)](https://bun.sh)
13
+ [![types included](https://img.shields.io/npm/types/ask-marcel-office-cli?logo=typescript&color=3178c6)](docs/USAGE.md)
15
14
 
16
- LLM tool-loops keep hitting the same three walls with Microsoft Graph:
15
+ Outlook · OneDrive · SharePoint · Calendar · Excel · Teams · Planner · To Do · OneNote · People
17
16
 
18
- 1. **Auth is a project.** Register an app, get tenant-admin consent, manage secrets, refresh tokens before the first API call.
19
- 2. **Default payloads are tuned for backend services, not context windows.** Listing endpoints return every field on every item, used-range Excel calls return four redundant 2D arrays, attachment endpoints inline base64 by default. An agent that reads "what's in my inbox" without trimming burns its budget on metadata it never needed.
20
- 3. **Errors are opaque.** `BadRequest: Invalid filter clause` doesn't tell a model what to fix.
17
+ [Install](#install-in-60-seconds) · [See it work](#see-it-work) · [What it reaches](#what-your-agent-can-reach) · [Files to markdown](#any-file-becomes-markdown) · [Library](#embed-it-as-a-typescript-library) · [All 184 commands](docs/COMMANDS.md)
21
18
 
22
- `ask-marcel-office` fixes all three at the CLI layer, so the model just calls commands and reads back-pressure-friendly responses.
19
+ </div>
23
20
 
24
- ## What you get
21
+ ---
25
22
 
26
- ### Read-only by design
23
+ Your agent is smart enough to answer *"what did Contoso say about the Q3 budget, and what's in the attached deck?"* It just can't see your mailbox.
27
24
 
28
- **This is the most important property.** 173 GET endpoints + 3 read-only POST (two searches + free/busy lookup) + 2 POST (create mail draft / create threaded reply draft) + 1 PATCH (update draft) = 179 commands. No `send-mail`, no `create-event`, no `upload-file`, no `delete-anything`. The only write operations are draft creation (a new mail or a threaded reply-all) and update — a hallucinated command can at most create an unsent draft in your Drafts folder. Safe default for autonomous agents, MCP servers, and "let Claude poke around my mailbox" sessions where you can't fully review every tool call.
25
+ `ask-marcel-office` gives any tool-calling LLM (Claude Code, Cursor, Cline, an MCP server, your own loop) eyes on your entire Microsoft 365:
29
26
 
30
- ### One call gets the full email context
27
+ ```bash
28
+ npm i -g ask-marcel-office-cli
29
+ ask-marcel-office login # your normal Microsoft sign-in, in a browser, once
30
+ ask-marcel-office list-mail-messages --top 5
31
+ ```
31
32
 
32
- A typical "read this email" loop in raw Graph: GET the message GET the attachments list GET each attachment's bytes scan the body HTML for `sharepoint.com` URLs resolve each URL to a driveItem GET each driveItem. Six round-trips minimum, plus HTML-to-text conversion the LLM has to do itself.
33
+ That is the whole setup. **No Azure app registration. No tenant-admin consent. No client secrets.** And nothing for a runaway agent to break: the command surface is read-only by design.
33
34
 
34
- `convert-mail-to-markdown` collapses that into one call:
35
+ ## The three walls it removes
35
36
 
36
- - Body rendered as markdown (turndown pipeline)
37
- - Quoted reply chains / forwarded-message blocks stripped by default so a long thread doesn't duplicate earlier messages into the model's context (the cut is replaced with a visible marker; opt out with `--keep-quoted true` to keep the full body)
38
- - Inline images embedded as base64 `data:` URIs (size-capped per image — opt out with `--inline-images false` to keep raw `cid:` refs)
39
- - File attachments listed below the body with id + name + size, ready for follow-up calls
40
- - Pair with `extract-sharepoint-links-in-mail` to resolve every SharePoint URL in the body to its driveItem in parallel (capped at 25 unique URLs per call)
37
+ ### 🔑 Sign in like a human, not like an app
41
38
 
42
- ### Office docs markdown or PDF on the fly
39
+ Microsoft Graph normally means registering an Azure app, chasing tenant-admin consent, and rotating client secrets before the first API call. Here, `login` drives a real browser window through the standard Microsoft sign-in (Playwright under the hood) and captures the same token the Teams web client already uses. Any Microsoft 365 account works, personal or enterprise. Tokens are cached at `~/.ask-marcel/token-cache.json` (0600) and refresh themselves headlessly; `scopes-check` reports per-token scopes and expiry with zero side effects.
43
40
 
44
- Feed any Office-shaped file (docx, xlsx, pptx, csv, rtf, odt, …) into the local conversion pipeline OR through Graph's `?format=pdf` when slide layout and images matter:
41
+ ### 🛡️ Safe to hand to an autonomous agent
45
42
 
46
- - `download-drive-item-as-markdown` docx via mammoth (embedded images become `[image]` placeholders by default — `--inline-images true` to embed them as base64, or pull the full-resolution originals with `extract-drive-item-images`), xlsx as one markdown table per sheet (a sheet whose used range exceeds the `--max-cells` cap, default 50 000, becomes a band-by-band read hint instead of a multi-hundred-MB table that would OOM), csv as a table, odt/ods/odp via content.xml (headings, lists, tables, named sheets, per-slide text, with `office:annotation` comments folded inline), **pptx** flattened to per-slide text (titles + bullets + text boxes + table cells, speaker notes inline, as `## Slide N` sections `download-drive-item-as-pdf` + a vision model when layout / images matter), **pdf** via text-layer extraction ([unpdf](https://github.com/unjs/unpdf) `text/plain`; a scanned / image-only PDF with no text layer points you at `download-drive-item-as-pdf` + a vision model), **legacy OLE Office** (`.xls` read by sheetjs like `.xlsx`; `.doc` extracted by [word-extractor](https://www.npmjs.com/package/word-extractor) as plain text; `.ppt` has no pure-JS path → convert to PDF first), plain-text passthrough
47
- - `download-drive-item-as-pdf` — Graph PDF conversion for anything it supports (preserves slide layout, images, charts — the right call for pptx and image-heavy docs)
48
- - `convert-mail-attachment-to-markdown` / `convert-mail-attachment-to-pdf` — same pipelines but starting from an email attachment
49
- - `read-mail-attachment` — one call that reads any mail attachment, auto-routing by file type (a `.zip` is unpacked and every entry converted; docx/xlsx/pptx/odf/csv/PDF/`.msg`/legacy/text → markdown; images, scanned PDFs, and legacy `.ppt` return an actionable 415 pointing at the raw-bytes / vision route) so an agent never has to choose between the `convert-mail-attachment-*` siblings
50
- - `convert-local-file` — same pipelines but starting from a file **on disk** (`--path ./report.docx`); never calls Graph (works offline, no login). A `.zip` is unpacked with every contained file converted in one call. The two things it can't do locally — convert **to** PDF and Loop/Fluid/Whiteboard sources — need Graph's server-side renderer (upload to OneDrive and use the drive-item siblings)
51
- - `convert-drive-item-zip` / `convert-mail-attachment-zip` — unzip an archive (OneDrive/SharePoint item, or an Outlook attachment) and convert **every** contained file in one call; legacy GBK / CP437 entry names (Chinese vendor archives from WinRAR / Windows Explorer) are decoded correctly, never mojibaked; unsupported entries are listed with a note instead of failing the archive
52
- - **Outlook `.msg` files** (saved/forwarded emails) convert to markdown through every entry point above — H1 subject, From/To/Cc/Date header block, the body, and an `## Attachments` section where each attachment is itself converted recursively (depth-capped)
53
- - `extract-sharepoint-links-in-documents` — the doc-side sibling of `extract-sharepoint-links-in-mail`: resolve every `*.sharepoint.com` URL embedded in a docx/xlsx/pptx (read from the package's relationship parts) or an odt/ods/odp (read from the inline `xlink:href` links in content.xml) to its driveItem, so an agent can follow references out of a document the same way it follows them out of an email
43
+ The 184 commands break down as 176 GET, 4 read-only POST (three searches and a free/busy lookup), and 4 mail-draft operations. No `send-mail`. No `create-event`. No `upload-file`. No `delete-anything`. The worst a hallucinated tool call can do is leave an unsent draft in your Drafts folder. That is the entire blast radius, which is why you can let an agent explore a mailbox without reviewing every call.
54
44
 
55
- Pass `--include-metadata true` on any `*-as-markdown` (or `convert-mail-attachment-to-markdown`) command to surface the side-channel content the rendered body hides. For **docx** (`## DOCX metadata`): core/app/custom doc properties, people registry, external hyperlinks, comments (each quoting the document text span it annotates), tracked changes, hidden text (`w:vanish`), MERGEFIELD / HYPERLINK / DOCVARIABLE instructions, bookmarks. For **xlsx** (`## Workbook metadata`): properties, external relationships, defined names, hidden / very-hidden sheets, legacy + threaded cell comments (each tagged with its cell), the persons registry. For **pptx** (`## PPTX metadata`): properties, external relationships, slide tags, comment authors + comments (legacy + modern, each anchored to its slide), and per-slide title / speaker notes / hidden flag — appended after the per-slide text body (use `download-drive-item-as-pdf` + a vision model for slide visuals / layout). Each family also covers its macro-enabled (`.docm` / `.xlsm` / `.pptm`) and template (`.dotx` / `.xltx` / `.potx`, etc.) variants, and surfaces a `### Macros (VBA)` section flagging an embedded `vbaProject.bin` (the file can execute code on open). For **OpenDocument** (`.odt` / `.ods` / `.odp`) the flag appends a `## OpenDocument metadata` block (Dublin Core + ODF properties, keywords, user-defined custom fields) after the converted body. No-op on other sources.
45
+ ### 🧠 Responses budgeted for a context window
56
46
 
57
- ### Extract embedded images from documents
47
+ Graph payloads are tuned for backend services. These are tuned for models. Listings return hand-picked fields instead of every property on every item (opt out with `--full true`). Email threads arrive with quoted reply chains stripped, which shrinks real threads by 78-90%. Inline images become `[inline image: logo.png]` placeholders instead of kilobytes of base64. Binaries over ~1 MB are refused inline and routed to disk via `--output-path`. And every failure returns `{error, hint, source, retryAfterSeconds}` so the model can repair its own call instead of retrying blind.
58
48
 
59
- `extract-drive-item-images` (OneDrive / SharePoint), `extract-mail-attachment-images` (Outlook attachments), and `extract-local-file-images` (a file **on disk** — no Graph, no login) pull the embedded images out of a **docx, xlsx, pptx, or pdf**. For Office files it reads the OOXML media parts (png/jpg/gif/bmp/tiff/webp/svg) — including original full-resolution / un-cropped originals and images on hidden slides that the rendered view never shows. SVG rides back as its XML source (which carries the diagram's own text labels); legacy vector (emf/wmf) and audio/video are skipped. For a PDF it walks every page via [unpdf](https://github.com/unjs/unpdf) (a pure-JS, no-native-deps pdf.js build) and re-encodes each painted image as PNG — page-oriented, so it captures images as drawn on each page (it does not reach layer-hidden/unpainted XObjects or the full uncropped original behind a clipped image). Pair with the global `--output-dir <dir>` to write every image to a folder (the directory is auto-created and each `base64` becomes a `savedTo` path); without it the bytes ride back base64-encoded so a vision model can read them directly.
49
+ ## See it work
60
50
 
61
- The CLI follows any SharePoint media-transform redirect internally, so the LLM never has to fetch an external URL.
51
+ Reading one email with its attachments in raw Graph: GET the message, GET the attachment list, GET each attachment's bytes, resolve every SharePoint link in the body, then run your own HTML-to-text pipeline. Six round trips minimum. Here:
62
52
 
63
- ### Find every drive you can reach
53
+ ```console
54
+ $ ask-marcel-office convert-mail-to-markdown --message-id "AAMkAD..."
64
55
 
65
- `list-drives` only returns your personal OneDrive(s). `list-accessible-drives` unions every discovery vector the delegated token can hit — `/me/drives` (personal), `/me/joinedTeams` (Teams libraries), `/me/memberOf` Unified groups → each group's drive (SharePoint M365-group sites), `/me/drive/sharedWithMe` (drives behind files shared with you), per-team `/teams/{id}/channels` → `filesFolder` for **private/shared channels** (which live in their own sites, not the team default drive), activity signals (`/me/drive/recent`, `/me/drive/following`, `/me/insights/{trending,used,shared}`), and every **non-default document library** of each discovered site via a path-addressed `/sites/{host}:/sites/{name}:/drives` (catches secondary libraries the default-drive vectors skip) — deduped by drive id and tagged with the `sources[]` that surfaced each one (`channel` = private/shared channel drive, `activity` = a recently-used/followed/trending item drive, `siteLibrary` = a non-default site library). These vectors catch OneDrives, channel sites, and direct-link sites the tenant search index (`search-sharepoint-sites-by-name`) never returns; the index in turn returns sites you can open but aren't a member of. For that index half, `search-all-accessible-sites` deep-pages the Microsoft Search API (`POST /search/query`, `entityTypes: ['site']`) past the single-page cap of `search-sharepoint-sites-by-name`, returning the *full* security-trimmed site index (on one tenant: ~154 sites vs 80). So **the union of `search-all-accessible-sites` + `list-accessible-drives` is the practical maximum on a delegated token** (truly enumerating *every* site in the tenant needs tenant-admin app-only `/sites/getAllSites`). Both site-search commands **exclude archived sites**: each result is probed (`GET /sites/{id}?$select=…,siteCollection`) and dropped when Graph reports it archived or fails with `423 resourceLocked` — the signal a departed/unlicensed user's auto-archived OneDrive returns (no more `sharepointerror.aspx?scenario=SiteArchived` dead links in the output); the count surfaces as `archivedExcluded`. `--max-groups` caps every fan-out, and `partialErrors[]` stays signal-only: benign "can't reach this one" results (404 no-drive, 403 access-denied / non-member channel, 423 admin-locked site, 400 stale id) are dropped silently — only actionable failures (auth, throttling, 5xx, network) are listed. Both commands also surface a best-effort `fileEstimate` — the Microsoft Search index's security-trimmed `driveItem` count, i.e. roughly how many files you can access across all of SharePoint/OneDrive (index-wide, not limited to the listed drives).
56
+ # Q3 budget review: action needed
57
+ **From:** Robin Chen <robin.chen@contoso.com>
66
58
 
67
- ### Browser-OAuth at first launch
59
+ Before Friday's review, the forecast tab still shows last quarter's
60
+ headcount. Can you sanity-check the attached numbers?
68
61
 
69
- No Azure app, no tenant admin. The CLI captures the same token the Teams web client uses — works for any Microsoft 365 account, personal or enterprise.
62
+ ## Attachments
63
+ - Q3-forecast.xlsx (48 KB) · id AAMkAD...
64
+ ```
70
65
 
71
- **Login flow:** the CLI drives a Playwright-launched Edge/Chrome window through the Teams sign-in, captures the tokens, and caches them at `~/.ask-marcel/token-cache.json` (0600).
66
+ The quoted chain below the reply is replaced by a one-line marker (restore it with `--keep-quoted true`), inline images become named placeholders, and attachments arrive with the ids ready for the follow-up call:
72
67
 
73
- ```bash
74
- ask-marcel-office login
68
+ ```console
69
+ $ ask-marcel-office convert-mail-attachment-to-markdown \
70
+ --message-id "AAMkAD..." --attachment-id "AAMkAD..."
75
71
  ```
76
72
 
77
- ### Stable error envelope with actionable hints
78
-
79
- 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:
80
74
 
81
- ### Lean responses
75
+ ```console
76
+ $ ask-marcel-office microsoft-search-query --query "Q3 budget filetype:pptx"
77
+ $ ask-marcel-office download-drive-item-as-markdown --drive-id "b!abc..." --item-id "01BYE..."
78
+ ```
82
79
 
83
- Listings ship with hand-tuned `--select` defaults — a mail listing returns id, subject, from, to, cc, dates, read-state, importance, bodyPreview rather than every field on every message. `get-excel-used-range` returns the `values` array instead of the four 2D arrays Graph emits. Opt out per call with `--full true`, or override with your own `--select id,subject,body`.
80
+ ## The difference in practice
84
81
 
85
- ### Saves big binaries to disk so they never hit the model's context
82
+ | You need | Raw Microsoft Graph | ask-marcel-office |
83
+ |---|---|---|
84
+ | Access | App registration, admin consent, secret rotation | `login`: one browser sign-in with your own account |
85
+ | An email + attachments | 6+ round trips, HTML-to-text is your problem | `convert-mail-to-markdown`: one call, markdown out |
86
+ | A pptx / pdf / legacy .doc as text | Raw bytes, bring your own converter | `download-drive-item-as-markdown`: one call |
87
+ | A useful error | `BadRequest: Invalid filter clause` | `hint: "string literals MUST use single quotes; embed one by doubling it"` |
88
+ | A 5 MB PDF | Base64 flooding the context window | `--output-path` writes it to disk; the model reads 3 lines |
89
+ | Throttling | HTTP 429, guess the backoff | `retryAfterSeconds` surfaced in the error envelope |
90
+
91
+ ## What your agent can reach
92
+
93
+ | Surface | Commands | In practice |
94
+ |---|---:|---|
95
+ | 📧 Outlook Mail | 36 | Search and read mail as markdown, convert any attachment (down to nested `.zip` and `.msg`), resolve SharePoint links in bodies, extract your signature, find existing drafts on a thread, prepare reply / forward drafts (the only writes) |
96
+ | 📁 OneDrive + SharePoint | 49 | Discover every drive and site your token can reach, search files, read any document as markdown or PDF, version history, share links resolved even into partner tenants where you're a guest |
97
+ | 📅 Calendar | 24 | "What's on this week" via relative dates (`today`, `start-of-week`, `+7d`), event details, free/busy lookups |
98
+ | 👥 People + directory | 16 | People search, user profiles, the directory around you, your own identity and IDs in one round trip |
99
+ | 💬 Teams | 16 | Your teams, channels, chats, and message history |
100
+ | ✅ Planner + To Do | 15 | Plans, buckets, tasks, checklists, due dates |
101
+ | 📊 Excel | 11 | Live workbook reads: worksheets, used ranges, tables (lean values, not Graph's four redundant 2D arrays) |
102
+ | 📓 OneNote | 11 | Notebooks, sections, page content |
103
+ | 🔎 Search + utilities | 6 | Federated Microsoft Search across the tenant, cursor pagination (`next-page`), token status (`scopes-check`), offline local-file conversion |
104
+
105
+ Full per-command tables with required parameters and Graph endpoints: **[docs/COMMANDS.md](docs/COMMANDS.md)**.
106
+
107
+ ## Any file becomes markdown
108
+
109
+ One conversion pipeline, four entry points: a OneDrive / SharePoint item, an Outlook attachment, a `.zip` archive, or a file on disk (`convert-local-file-to-markdown` runs fully offline, no login).
110
+
111
+ | Source | What comes back |
112
+ |---|---|
113
+ | docx / docm / dotx | Clean markdown; `--include-metadata true` adds comments, tracked changes, hidden text, and a VBA-macro flag |
114
+ | xlsx / xlsm / csv | One markdown table per sheet, with a 50 000-cell guard so a huge sheet becomes a band-by-band read plan instead of an OOM |
115
+ | pptx / pptm | Per-slide text with speaker notes (`## Slide N`); switch to the PDF sibling + a vision model when layout matters |
116
+ | pdf | Text-layer extraction; a scanned PDF answers with a pointer to the vision route instead of silence |
117
+ | odt / ods / odp | Headings, lists, tables, per-slide text, comments folded inline |
118
+ | Legacy .doc / .xls | `.xls` reads like `.xlsx`; `.doc` extracts as plain text (`.ppt`: convert to PDF first) |
119
+ | Outlook .msg | The full email: headers, quote-stripped body, and every attachment converted recursively |
120
+ | .zip | Every entry converted in one call; GBK / CP437 entry names decoded, never mojibake |
121
+ | Loop / Whiteboard | Rendered through Graph's server-side converter |
122
+
123
+ Need the pictures instead of the words? `extract-drive-item-images`, `extract-mail-attachment-images`, and `extract-local-file-images` pull the embedded images out of docx / xlsx / pptx / pdf (including full-resolution originals and images on hidden slides), ready for a vision model; `--output-dir` writes them straight to disk.
124
+
125
+ ## Designed for the agent loop
126
+
127
+ - **Self-teaching.** `help-json --terse` returns a slim JSON manifest built for a model's first contact; add `--category mail` to scope it. `docs <command>` prints one command's full documentation (response shape, examples, the underlying Graph endpoint). Scan, pick, read, call.
128
+ - **Errors that repair the call.** Every failure is `{ok: false, error, errorCode?, hint?, source, retryAfterSeconds?}`, with curated hints for 20+ recurring Graph mistakes, including wrong-resolver pointers (a Teams URL passed to `resolve-mail-link` answers with the command to use instead).
129
+ - **Pagination without state.** Every listing returns an opaque cursor; `next-page --url "<cursor>"` continues any of them.
130
+ - **Relative dates.** `--start-date-time start-of-week --end-date-time +7d`. No timestamp math before "what's on my calendar".
131
+ - **Binary discipline.** Multi-MB payloads never hit stdout by accident: without `--output-path`, a binary answer is a one-line summary; with it, the bytes land on disk and the envelope carries `savedTo`.
132
+ - **Tenant-wide reach.** `list-accessible-drives` unions every discovery vector a delegated token can hit (Teams libraries, group sites, private channels, shared-with-me, activity signals, secondary site libraries) and `search-all-accessible-sites` deep-pages the search index. Together: the practical maximum reachable without tenant-admin rights.
133
+
134
+ ## Install in 60 seconds
86
135
 
87
- A typical conversion command returns multi-MB PDF bytes. Sending 5 MB of base64 through stdout would blow most context windows AND quadruple the token bill.
136
+ ```bash
137
+ # Bun >=1.0 or Node >=20 · macOS, Windows, Linux
138
+ npm i -g ask-marcel-office-cli
88
139
 
89
- Two flag patterns avoid the round-trip:
140
+ ask-marcel-office login # browser opens once, tokens cached
141
+ ask-marcel-office my-quick-context # who am I + my IDs, one round trip
142
+ ask-marcel-office list-calendar-view --start-date-time today --end-date-time +7d
143
+ ask-marcel-office search-onedrive-files --drive-id "b!abc..." --query "Q3 budget"
144
+ ```
90
145
 
91
- - **`--output-path /path/to/file.pdf`** the CLI decodes the bytes, writes them to disk, and replaces `base64: "..."` in the envelope with `savedTo: "/path/to/file.pdf"`. The LLM sees a 3-line confirmation instead of a 7-million-character payload. Works on every command that returns binary or text content; rejected with a clear error on plain-JSON commands so a misapplied flag is never silent. A binary payload over ~1 MB is **refused** without this flag (an `inline_too_large` error pointing you here), so a multi-MB base64 string can never flood the context by accident.
92
- - **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
93
147
 
94
- ### Relative dates on calendar windows
148
+ Any agent that can run a shell command can use the whole surface today: point it at `help-json --terse`, let it call commands with `--output json`, and the lean defaults plus structured hints let it recover from its own mistakes. No wrapper required.
95
149
 
96
- `--start-date-time "start-of-week" --end-date-time "+7d"`. No timestamp math before answering "what's on my calendar this week".
150
+ **No shell? Register it as an MCP server.** It speaks MCP over stdio, so any MCP client can drive it — straight off the npm registry via `npx`, nothing to clone or preinstall.
97
151
 
98
- ## 30-second quickstart
152
+ **Claude Code** — one command:
99
153
 
100
154
  ```bash
101
- # install (Bun ≥1.0 or Node ≥20)
102
- npm i -g ask-marcel-office-cli
103
-
104
- # authenticate (cached → refresh → browser fallback)
105
- ask-marcel-office login
106
-
107
- # the rest is read-only (the only writes are mail drafts) and discoverable from --help
108
- ask-marcel-office list-drives
109
- ask-marcel-office search-onedrive-files --drive-id "b!abc..." --query "Q3 budget"
110
- ask-marcel-office convert-mail-to-markdown --message-id "AAMkAD..."
111
- ask-marcel-office list-calendar-view --start-date-time today --end-date-time +7d
112
- ask-marcel-office convert-mail-attachment-to-pdf \
113
- --message-id "AAMkAD..." --attachment-id "AAMkAD...attach1" \
114
- --output-path /tmp/deck.pdf
155
+ claude mcp add --transport stdio --scope user ask-marcel-office -- npx -y ask-marcel-office-cli mcp
115
156
  ```
116
157
 
117
- ## Asking the CLI what it can do
158
+ **Claude Desktop, Cursor, or any other MCP client** — they all take the same `mcpServers` block; only the file location differs:
159
+
160
+ | Client | Config file |
161
+ |:--|:--|
162
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
163
+ | Claude Desktop (Windows) | `%AppData%\Claude\claude_desktop_config.json` |
164
+ | Cursor, Cline, Windsurf, … | see your client's MCP docs — the block below is unchanged |
165
+
166
+ ```json
167
+ {
168
+ "mcpServers": {
169
+ "ask-marcel-office": {
170
+ "description": "Ask Marcel Office CLI MCP server",
171
+ "command": "npx",
172
+ "args": ["-y", "ask-marcel-office-cli", "mcp"],
173
+ "env": {}
174
+ }
175
+ }
176
+ }
177
+ ```
118
178
 
119
- Five discovery surfaces, each tuned for a different audience and token budget:
179
+ > **Two things to know.** The first launch downloads the package into npx's cache, so allow it one slow start; every start after that is instant. And GUI apps don't inherit your shell's `PATH`: terminal-launched clients resolve `"npx"` fine, but a client launched from the Dock or Start menu (Claude Desktop) may not find it — if the server won't start, replace `"command": "npx"` with the absolute path `which npx` prints. Bun users: `bunx` works in place of `npx -y`.
120
180
 
121
- | When you want | Run | Returns |
122
- |---|---|---|
123
- | Help with a single command | `ask-marcel-office <command> --help` | Required flags, optional flags, an example, pagination notes |
124
- | A scan of every command | `ask-marcel-office --help` | One-sentence summary per command, grouped by category |
125
- | 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 |
126
- | 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 |
127
- | 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:
128
182
 
129
- Pair `help-json --terse --category <name>` with `docs <command>` for the canonical agent loop: scan the category, pick a command, fetch its full docs, then call it.
183
+ | Tool | Does |
184
+ |:--|:--|
185
+ | `list-commands` | The terse manifest. Start here; `category` narrows it. |
186
+ | `get-command-docs` | Full docs for one command: options, endpoint, example. |
187
+ | `run-command` | The 180 **read** commands. `readOnlyHint: true`, so clients can auto-approve it. |
188
+ | `run-write-command` | The 4 mail-draft **write** commands. Separate tool so the read tool's promise stays honest. |
189
+ | `login` | Sign in / refresh. Opens a browser on this machine. |
130
190
 
131
- ## Use it from Claude Code, Cursor, Cline, or any tool-calling LLM
191
+ **Sign in from a terminal first** `npx -y ask-marcel-office-cli login`. Do this once before wiring up any client, and the reads just work.
132
192
 
133
- Most agents already know how to read JSON from stdout. Two patterns work:
193
+ ## Embed it as a TypeScript library
134
194
 
135
- **1. Drop in as a shell tool** — the agent learns the manifest, then runs `ask-marcel-office <command> --output json`. The slim defaults + structured error hints mean it can self-recover from typos.
136
-
137
- **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:
138
196
 
139
197
  ```ts
140
198
  import { commands, buildDeps } from 'ask-marcel-office-cli';
@@ -142,57 +200,59 @@ import { commands, buildDeps } from 'ask-marcel-office-cli';
142
200
  const { graph } = buildDeps();
143
201
  const result = await commands['list-mail-messages'].execute(graph, { top: '10' });
144
202
  if (result.ok) {
145
- // result.value is the Graph payload — typed Result<unknown, GraphError>
203
+ // result.value is the Graph payload
146
204
  }
147
205
  ```
148
206
 
149
- ### Auth two paths
150
-
151
- **Most users — use the built-in browser-OAuth ladder:**
152
-
153
- ```ts
154
- import { buildDeps } from 'ask-marcel-office-cli';
155
-
156
- const { graph } = buildDeps();
157
- // First call triggers cache → refresh → headed-Chromium fallback automatically.
158
- // Tokens cached at ~/.ask-marcel/token-cache.json for subsequent calls.
159
- ```
160
-
161
- **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`.
162
208
 
163
209
  ```ts
164
210
  import { createGraphClient } from 'ask-marcel-office-cli';
165
211
 
212
+ const cancelled = async () => ({ ok: false as const, error: { type: 'auth_cancelled' as const } });
213
+
166
214
  const graph = createGraphClient({
167
- getAccessToken: async () => ({
168
- ok: true,
169
- value: await fetchTokenFromYourVault(),
170
- }),
215
+ getAccessToken: async () => ({ ok: true, value: await fetchTokenFromYourVault() }),
171
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,
172
226
  });
173
227
  ```
174
228
 
175
- The `AuthManager` interface is two async methods that return `Result<T, AuthError>`. Plug in any token source — Azure Managed Identity, a secrets vault, an on-behalf-of flow, hand-pasted JWTs in tests. The Graph client doesn't care where the token came from.
229
+ Azure Managed Identity, an on-behalf-of flow, hand-pasted JWTs in tests: the Graph client doesn't care where the token came from. Full library guide: **[docs/USAGE.md](docs/USAGE.md)**.
176
230
 
177
231
  ## Deep docs
178
232
 
179
- - **[All 179 commands](docs/COMMANDS.md)** per-category tables with required params + Graph endpoint
180
- - **[Usage guide](docs/USAGE.md)** output formats, OData passthrough, `--output-path`, pagination, library API, architecture, configuration, quality gates
181
- - **[Machine-readable manifest](docs/commands.json)** JSON for programmatic discovery (LLM tool-loops, IDE plugins, MCP servers); also importable via `import manifest from 'ask-marcel-office-cli/commands.json'`
182
- - **[QA playbook](docs/QA-PLAYBOOK.md)** the repeatable full-surface health-check procedure (offline gates, parameter matrix, conversion contracts, live Graph drift probes) used to audit each release
233
+ - **[All 184 commands](docs/COMMANDS.md)**: per-category tables with required params + Graph endpoint
234
+ - **[Usage guide](docs/USAGE.md)**: output formats, OData passthrough, `--output-path`, pagination, library API, architecture, configuration
235
+ - **[Machine-readable manifest](docs/commands.json)**: JSON for programmatic discovery, also importable via `import manifest from 'ask-marcel-office-cli/commands.json'`
236
+ - **[QA playbook](docs/QA-PLAYBOOK.md)**: the repeatable full-surface health check run before each release
183
237
 
184
238
  ## Roadmap
185
239
 
186
- Read-only stays the default forever. There's no fixed feature backlog — coverage grows out of real LLM workflows as they come up.
187
-
188
- 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).
189
241
 
190
242
  ## Built with
191
243
 
192
- - **Bun + TypeScript** single binary install, Node 20 fallback. `Result<T, E>` at every IO boundary, branded value-object types at trust boundaries, classicist outside-in TDD, zero lint warnings, 100% coverage on every tier.
193
- - **Microsoft Graph v1.0** the public API surface, no beta endpoints in production code.
194
- - **Playwright** headed Chromium for the first-launch browser-OAuth dance.
244
+ - **Bun + TypeScript**: single-package install, Node >=20 fallback, `Result<T, E>` at every IO boundary, a 100% coverage gate on every tier
245
+ - **Microsoft Graph v1.0**: the public API surface, no beta endpoints in production code
246
+ - **Playwright**: the headed browser behind the one-time sign-in
195
247
 
196
248
  ## License
197
249
 
198
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>