ask-marcel-office-cli 1.0.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +153 -384
  3. package/dist/cli.js +13222 -94112
  4. package/dist/commands.json +1500 -135
  5. package/dist/composition/build-deps.d.ts +13 -0
  6. package/dist/composition/cli.d.ts +8 -0
  7. package/dist/domain/iso-datetime.d.ts +30 -0
  8. package/dist/domain/utilities/archive-status.d.ts +18 -0
  9. package/dist/domain/utilities/site-url-classifier.d.ts +15 -0
  10. package/dist/index.js +10912 -92275
  11. package/dist/infra/auth.d.ts +56 -3
  12. package/dist/infra/browser-auth.d.ts +100 -10
  13. package/dist/infra/graph-client.d.ts +40 -0
  14. package/dist/infra/legacy-doc-extractor.d.ts +14 -0
  15. package/dist/infra/msg-reader-adapter.d.ts +83 -0
  16. package/dist/infra/network-error.d.ts +9 -0
  17. package/dist/infra/ooxml-media-extractor.d.ts +27 -0
  18. package/dist/infra/ooxml-zip-adapter.d.ts +28 -0
  19. package/dist/infra/pdf-image-extractor.d.ts +18 -0
  20. package/dist/infra/pdf-text-extractor.d.ts +41 -0
  21. package/dist/infra/png-encode.d.ts +15 -0
  22. package/dist/infra/sheetjs-adapter.d.ts +1 -1
  23. package/dist/infra/system-browser-auth.d.ts +35 -0
  24. package/dist/infra/system-browser-loader.d.ts +3 -0
  25. package/dist/infra/token-callback-server.d.ts +31 -0
  26. package/dist/infra/zip-reader.d.ts +32 -0
  27. package/dist/presenter/error-hints.d.ts +41 -0
  28. package/dist/presenter/output.d.ts +2 -1
  29. package/dist/use-cases/commands/build-command.d.ts +30 -8
  30. package/dist/use-cases/commands/command-types.d.ts +53 -1
  31. package/dist/use-cases/commands/convert-calendar-event-attachment-to-markdown.d.ts +15 -0
  32. package/dist/use-cases/commands/{download-drive-item-version-as-pdf.d.ts → convert-calendar-event-attachment-to-pdf.d.ts} +2 -3
  33. package/dist/use-cases/commands/convert-drive-item-zip.d.ts +26 -0
  34. package/dist/use-cases/commands/convert-local-file.d.ts +37 -0
  35. package/dist/use-cases/commands/convert-mail-attachment-to-markdown.d.ts +7 -1
  36. package/dist/use-cases/commands/convert-mail-attachment-to-pdf.d.ts +2 -1
  37. package/dist/use-cases/commands/convert-mail-attachment-zip.d.ts +24 -0
  38. package/dist/use-cases/commands/convert-mail-to-markdown.d.ts +8 -0
  39. package/dist/use-cases/commands/create-mail-draft.d.ts +22 -0
  40. package/dist/use-cases/commands/doc-to-markdown.d.ts +14 -0
  41. package/dist/use-cases/commands/docs-render.d.ts +7 -2
  42. package/dist/use-cases/commands/docs.d.ts +44 -1
  43. package/dist/use-cases/commands/docx-comment-anchors.d.ts +22 -0
  44. package/dist/use-cases/commands/docx-metadata-to-markdown.d.ts +3 -0
  45. package/dist/use-cases/commands/docx-metadata.d.ts +69 -0
  46. package/dist/use-cases/commands/docx-to-markdown.d.ts +8 -3
  47. package/dist/use-cases/commands/download-drive-item-as-markdown.d.ts +9 -0
  48. package/dist/use-cases/commands/download-drive-item-version.d.ts +21 -0
  49. package/dist/use-cases/commands/{download-drive-item-version-as-markdown.d.ts → extract-drive-item-images.d.ts} +0 -1
  50. package/dist/use-cases/commands/extract-local-file-images.d.ts +21 -0
  51. package/dist/use-cases/commands/{download-drive-item-version-content.d.ts → extract-mail-attachment-images.d.ts} +2 -3
  52. package/dist/use-cases/commands/extract-sharepoint-links-in-documents.d.ts +33 -0
  53. package/dist/use-cases/commands/extract-sharepoint-links-in-mail.d.ts +1 -8
  54. package/dist/use-cases/commands/fetch-raw-bytes.d.ts +2 -1
  55. package/dist/use-cases/commands/file-counts.d.ts +7 -0
  56. package/dist/use-cases/commands/filter-archived-sites.d.ts +17 -0
  57. package/dist/use-cases/commands/find-chats-with-user.d.ts +10 -0
  58. package/dist/use-cases/commands/first-sentence.d.ts +12 -0
  59. package/dist/use-cases/commands/get-chat.d.ts +1 -5
  60. package/dist/use-cases/commands/get-excel-chart-image.d.ts +20 -0
  61. package/dist/use-cases/commands/get-excel-used-range.d.ts +7 -2
  62. package/dist/use-cases/commands/get-onenote-page-as-markdown.d.ts +8 -0
  63. package/dist/use-cases/commands/get-teams-chat-message.d.ts +9 -0
  64. package/dist/use-cases/commands/get-todo-task.d.ts +9 -2
  65. package/dist/use-cases/commands/image-extraction.d.ts +10 -0
  66. package/dist/use-cases/commands/iso-datetime-schema.d.ts +21 -0
  67. package/dist/use-cases/commands/link-shape.d.ts +13 -0
  68. package/dist/use-cases/commands/list-accessible-drives.d.ts +14 -0
  69. package/dist/use-cases/commands/list-calendar-event-attachments.d.ts +14 -0
  70. package/dist/use-cases/commands/list-calendar-view-delta.d.ts +2 -2
  71. package/dist/use-cases/commands/list-teams-chat-history.d.ts +16 -0
  72. package/dist/use-cases/commands/list-teams-chat-messages.d.ts +8 -0
  73. package/dist/use-cases/commands/list-teams-chats-with-messages.d.ts +9 -0
  74. package/dist/use-cases/commands/mail-quote-stripper.d.ts +25 -0
  75. package/dist/use-cases/commands/markdown-dispatch.d.ts +29 -0
  76. package/dist/use-cases/commands/markdown-pipeline.d.ts +6 -2
  77. package/dist/use-cases/commands/media-files.d.ts +20 -0
  78. package/dist/use-cases/commands/msg-to-markdown.d.ts +28 -0
  79. package/dist/use-cases/commands/odf-content-to-markdown.d.ts +5 -0
  80. package/dist/use-cases/commands/odf-metadata-to-markdown.d.ts +3 -0
  81. package/dist/use-cases/commands/odf-metadata.d.ts +26 -0
  82. package/dist/use-cases/commands/odf-to-markdown.d.ts +17 -0
  83. package/dist/use-cases/commands/office-extensions.d.ts +13 -0
  84. package/dist/use-cases/commands/office-to-markdown.d.ts +6 -2
  85. package/dist/use-cases/commands/onenote-metadata.d.ts +21 -0
  86. package/dist/use-cases/commands/onenote-resource-embedder.d.ts +3 -0
  87. package/dist/use-cases/commands/ooxml-metadata-to-markdown.d.ts +16 -0
  88. package/dist/use-cases/commands/ooxml-metadata.d.ts +23 -0
  89. package/dist/use-cases/commands/ooxml-xml-walker.d.ts +36 -0
  90. package/dist/use-cases/commands/option-descriptions.d.ts +5 -0
  91. package/dist/use-cases/commands/output-path.d.ts +23 -0
  92. package/dist/use-cases/commands/pdf-to-markdown.d.ts +19 -0
  93. package/dist/use-cases/commands/pptx-comments.d.ts +24 -0
  94. package/dist/use-cases/commands/pptx-metadata-to-markdown.d.ts +3 -0
  95. package/dist/use-cases/commands/pptx-metadata.d.ts +33 -0
  96. package/dist/use-cases/commands/pptx-slides.d.ts +17 -0
  97. package/dist/use-cases/commands/pptx-to-markdown.d.ts +7 -0
  98. package/dist/use-cases/commands/read-mail-attachment.d.ts +15 -0
  99. package/dist/use-cases/commands/resolve-calendar-link.d.ts +8 -0
  100. package/dist/use-cases/commands/resolve-drive-share-link.d.ts +8 -0
  101. package/dist/use-cases/commands/resolve-mail-link.d.ts +8 -0
  102. package/dist/use-cases/commands/resolve-teams-link.d.ts +8 -0
  103. package/dist/use-cases/commands/search-all-accessible-sites.d.ts +12 -0
  104. package/dist/use-cases/commands/search-index-total.d.ts +3 -0
  105. package/dist/use-cases/commands/search-sharepoint-sites-by-name.d.ts +3 -2
  106. package/dist/use-cases/commands/sharepoint-link-extractor.d.ts +17 -12
  107. package/dist/use-cases/commands/text-passthrough.d.ts +15 -2
  108. package/dist/use-cases/commands/todo-parse-uri-rewrite.d.ts +7 -0
  109. package/dist/use-cases/commands/update-mail-draft.d.ts +22 -0
  110. package/dist/use-cases/commands/version-id.d.ts +7 -6
  111. package/dist/use-cases/commands/xlsx-comments.d.ts +29 -0
  112. package/dist/use-cases/commands/xlsx-metadata-to-markdown.d.ts +3 -0
  113. package/dist/use-cases/commands/xlsx-metadata.d.ts +39 -0
  114. package/dist/use-cases/commands/xlsx-to-markdown.d.ts +9 -2
  115. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +30 -0
  116. package/dist/use-cases/ports/filesystem.d.ts +10 -0
  117. package/docs/COMMANDS.md +259 -0
  118. package/docs/USAGE.md +266 -0
  119. package/docs/commands.json +1500 -135
  120. package/package.json +16 -8
  121. /package/dist/use-cases/commands/{download-onedrive-file-content.d.ts → download-drive-item-content.d.ts} +0 -0
package/README.md CHANGED
@@ -1,466 +1,235 @@
1
1
  # ask-marcel-office-cli
2
2
 
3
- Microsoft Graph CLI designed for LLM consumption via skills. Explicit commands, token-lean YAML-ish text output by default (JSON envelope on `--output json` for tool-chaining), zero interactive prompts beyond auth.
4
-
5
- ## Commands
6
-
7
- ### Authentication
8
-
9
- | Command | Description |
10
- |---------|-------------|
11
- | `login` | Authenticate (cached → refresh → browser fallback) |
12
- | `logout` | Clear cached tokens |
13
- | `update` | Update ask-marcel to the latest version on npm (auto-detects npm vs bun) |
14
- | `docs <cmd>` | Print Markdown docs for a single command (full machine-readable manifest at [`docs/commands.json`](docs/commands.json) or via `import manifest from 'ask-marcel-office-cli/commands.json'`) |
15
-
16
- <!-- AUTO-GENERATED-COMMANDS:BEGIN -->
17
-
18
- ### OneDrive Files
19
-
20
- | Command | Description | Required params | Graph endpoint |
21
- |---------|-------------|-----------------|----------------|
22
- | `download-drive-item-as-markdown` | Download a OneDrive / SharePoint file converted to markdown via local conversion pipelines. Supported: docx (mammoth → turndown, with inline images as data: URIs and tables as GFM pipe tables), xlsx (one markdown table per sheet via sheetjs), csv (rendered as a markdown table), plus plain-text passthrough (txt/md/html/json/yaml/log/xml/etc.) — the bytes are followed through any CDN redirect and returned inline as `{ contentType: "text/plain", size, text }` so the LLM never needs a separate fetch step. Loop/Fluid/Whiteboard files use Graph `?format=html` (the four inputs Microsoft documents — https://learn.microsoft.com/en-us/graph/api/driveitem-get-content-format). For pptx use `download-drive-item-as-pdf` — Graph PDF preserves slide layout, and a vision-capable LLM reads it more reliably than flattened bullets. For pdf/rtf/odt/etc. also use `download-drive-item-as-pdf` — Graph `?format=pdf` accepts 38 input extensions. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content?format=html` |
23
- | `download-drive-item-as-pdf` | Download a OneDrive / SharePoint file converted to PDF on the fly by Graph (`?format=pdf`). Source must be one of the Office formats Graph supports — doc, docx, ppt, pptx, xls, xlsx, rtf, csv, odp, ods, odt, etc. The command pre-fetches the filename and short-circuits to a raw download in two cases: plain-text source extensions (txt, md, html, json, …) where conversion is meaningless, and `pdf` sources where the source IS already a PDF (Graph’s `?format=pdf` does not list `pdf` in its supported input set — the CDN responds 406 InputFormatNotSupported on `pdf → pdf`). Worst-case wall-clock is two 60s round-trips back-to-back. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content?format=pdf` |
24
- | `download-drive-item-version-as-markdown` | Download a *historical version* of a OneDrive / SharePoint file converted to markdown. Same local conversion pipeline as `download-drive-item-as-markdown`: docx via mammoth, xlsx via sheetjs (markdown tables per sheet), csv as a markdown table, plus plain-text passthrough. Uses an elevated Graph token (captured at login from m365.cloud.microsoft / M365ChatClient) for the bytes-fetch, since the Teams web client token cannot fetch historical-version stream content (returns 403 logicalPermissionAccessDenied). For pptx use `download-drive-item-version-as-pdf`. Loop/Fluid/Whiteboard use Graph `?format=html` (the four inputs Microsoft documents). | `--drive-id`, `--item-id`, `--version-id` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content?format=html` |
25
- | `download-drive-item-version-as-pdf` | Convert a *historical version* of a OneDrive / SharePoint file to PDF and return the bytes inline. Same shape as `download-drive-item-as-pdf` plus a `--version-id`. The CLI uses an ODSP-elevated token (M365ChatClient identity captured at login) for both the Graph call and the CDN-redirect follow, so the LLM never has to fetch an external URL. Plain-text source extensions and `pdf` sources short-circuit to a raw-bytes return. Note: Graph's `?format=pdf` sometimes serves the *current* version through this endpoint, but not reliably — audit v1.0.0 §D4 saw it fall back to raw source bytes (`passthrough: true`) even for the current version on the test tenant. For the current version always use `download-drive-item-as-pdf` so you don't depend on this quirk. When the response carries `passthrough: true`, the bytes are the source file (not a PDF); save them with the source extension, not `.pdf` — the global output-path flag refuses the mismatch (audit §B4). | `--drive-id`, `--item-id`, `--version-id` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content?format=pdf` |
26
- | `download-drive-item-version-content` | Download the bytes of a *non-current* historical version of a OneDrive / SharePoint file, inlined. Graph refuses to serve the current version through this endpoint with "You cannot get the content of the current version" — for the current version use `download-onedrive-file-content`. The CLI follows the SharePoint streamContent redirect internally using an M365ChatClient-elevated token (captured at login) so the LLM never has to fetch an external URL. | `--drive-id`, `--item-id`, `--version-id` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content` |
27
- | `download-onedrive-file-content` | Download the binary content of a file stored in OneDrive / SharePoint, with the bytes inlined. The CLI follows the Graph 302 → SharePoint media-transform redirect internally so the LLM never has to fetch an external URL. Pre-checks the filename: if it matches the plain-text set (txt/md/html/json/yaml/log/xml/etc.), decodes the bytes as UTF-8 and returns `{contentType: "text/plain", size, text}` instead of base64 — avoids ~33% bloat on text payloads. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
28
- | `get-drive-delta` | Get the incremental change set (added / modified / deleted items) under a OneDrive / SharePoint folder. Use the `@odata.deltaLink` from a previous response to resume. | `--drive-id`, `--item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/delta()` |
29
- | `get-drive-item` | Get the metadata (driveItem resource) of a single file or folder in OneDrive / SharePoint. Use `--select` to slim the response — a full driveItem can run >10 KB with all the optional facets. | `--drive-id`, `--item-id`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}` |
30
- | `get-drive-item-analytics` | Return view / activity analytics for a OneDrive / SharePoint file — `allTime` totals (views, viewers) and `lastSevenDays` rollup. Useful for ranking files by attention or detecting stale content. **Known empty case**: returns `{ allTime: null, lastSevenDays: null }` on low-traffic items, or when the calling identity (the Teams web client basic token) lacks the analytics scope on the tenant. Do not interpret nulls as "no views" — interpret as "not available for this caller". For active files where you expect data and see nulls, escalate to a token with `Reports.Read.All`. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/analytics` |
31
- | `get-drive-item-created-by-user` | Return the `user` resource for whoever created a OneDrive / SharePoint file — full profile, not just the truncated `createdBy.user` summary embedded in the parent driveItem. Useful when you need title / department / mail of the author. Use `--select` to fetch only the fields you care about (e.g. `--select id,displayName,jobTitle,department,mail`). | `--drive-id`, `--item-id`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/createdByUser` |
32
- | `get-drive-item-last-modified-by-user` | Return the full `user` resource for whoever last modified a OneDrive / SharePoint file — sibling to `get-drive-item-created-by-user`. Use `--select` to fetch only specific fields. | `--drive-id`, `--item-id`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/lastModifiedByUser` |
33
- | `get-drive-root-delta` | Track incremental changes (added / modified / deleted items) anywhere under the signed-in user's OneDrive root. **Takes zero required arguments** — acts implicitly on the signed-in user's primary OneDrive; use `get-drive-delta` to target a specific drive by ID. The first call returns a snapshot plus `@odata.deltaLink`; subsequent calls with that link return only what has changed since. Cross-folder companion to `get-drive-delta` (which scopes to one specific folder). | `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/drive/root/delta()` |
34
- | `get-drive-root-item` | Get the root folder (driveItem) of a OneDrive / SharePoint drive. Use `--select` to slim the response (e.g. `--select id,name,folder`). | `--drive-id`, `--select`, `--expand` | `GET /drives/{drive-id}/root` |
35
- | `get-drive-special-folder` | Resolve a OneDrive well-known folder via `--folder-name` (one of `documents`, `photos`, `cameraroll`, `approot`, `music`, `attachments`) without having to navigate from the root. Returns the folder's driveItem (id, name, parentReference, etc.) ready to feed into `list-folder-files` or `download-onedrive-file-content`. | `--folder-name`, `--select`, `--expand` | `GET /me/drive/special/{folder-name}` |
36
- | `list-drive-item-permissions` | List the sharing permissions on a OneDrive / SharePoint file or folder. | `--drive-id`, `--item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/permissions` |
37
- | `list-drive-item-thumbnails` | List thumbnail URLs (small / medium / large) for a OneDrive / SharePoint file. Each thumbnail set has pre-signed CDN URLs you can render in a UI without further auth. | `--drive-id`, `--item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/thumbnails` |
38
- | `list-drive-item-versions` | List the historical versions of a OneDrive / SharePoint file (each save creates a new version). Note: each version's `id` is a stringified float like `"79.0"` (NOT an integer like `79`) — pass it literally to sibling commands such as `download-drive-item-version-content` / `-as-pdf` / `-as-markdown`; numeric coercion silently fails because Graph rejects `79` against a path templated as `{version-id}`. | `--drive-id`, `--item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/versions` |
39
- | `list-drives` | List all OneDrive / SharePoint drives the signed-in user has access to. On personal accounts this returns only the user's primary OneDrive (single entry in `value[]`); on tenanted accounts it includes every drive the user can reach including delegated mailboxes and shared SharePoint document libraries. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/drives` |
40
- | `list-folder-files` | List the children (files and subfolders) of a folder in OneDrive / SharePoint. | `--drive-id`, `--item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/children` |
41
- | `list-followed-drive-items` | List driveItems the signed-in user has explicitly followed (the OneDrive star). A small, hand-curated set of frequently-revisited files, distinct from the algorithmic `list-recent-files` and `list-recently-used-insights`. | `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/drive/following` |
42
- | `list-recent-files` | List the signed-in user's most recently used / opened OneDrive and SharePoint files, ranked by Microsoft's recency signal. The strongest single answer to "what is this user working on right now?". Note: Graph's recent-files feed is signal-driven and can lag the underlying drive by 24-48 hours — `lastModifiedDateTime` here may be older than the file's true mtime. For "what is the actual latest version?" call `list-drive-item-versions` on a specific item. | `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/drive/recent` |
43
- | `list-recently-used-insights` | List documents the signed-in user has *personally* used recently (Microsoft's machine-learning recency signal — distinct from `list-recent-files` which is the OneDrive recency feed). Returns `usageDetails` with `lastAccessedDateTime` + `lastModifiedDateTime`. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/insights/used` |
44
- | `list-shared-insights` | List documents *shared with* the signed-in user, scored by Microsoft's relevance ranking — sibling to `list-shared-with-me` but with sharing-context details (`sharingHistory[]`, `lastShared.sharedBy`, `lastShared.sharingReference`). | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/insights/shared` |
45
- | `list-shared-with-me` | List driveItems shared with the signed-in user (typically by colleagues). Each entry includes the original drive + item ID under `remoteItem` so you can chain into `get-drive-item`, `download-onedrive-file-content`, etc. Note: Graph does NOT honor any OData query parameters on this endpoint (top/select/filter/etc. are all silently ignored), so the CLI does not advertise them. The full collection (~500 items in a typical tenant) is always returned; slice client-side or pair with the global output-path flag to land the raw JSON on disk. | _(none)_ | `GET /me/drive/sharedWithMe` |
46
- | `list-trending-insights` | List documents trending around the signed-in user — files popular in their working network (colleagues' recent edits, shares, opens). Microsoft's relevance ranking, useful for surfacing unfamiliar but related work. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/insights/trending` |
47
- | `search-my-documents` | Search the signed-in user’s default OneDrive for documents matching a free-text query (filename, content, metadata). | `--query`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/drive/search(q='{query}')` |
48
- | `search-onedrive-files` | Search a single OneDrive / SharePoint drive for files and folders matching a free-text query. | `--drive-id`, `--query`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/search(q='{query}')` |
49
-
50
- ### Excel (workbook files)
51
-
52
- | Command | Description | Required params | Graph endpoint |
53
- |---------|-------------|-----------------|----------------|
54
- | `get-excel-range` | Get the cell values, formulas, and formats of a specific Excel range (e.g. `A1:C10`). The CLI caps the in-flight range at 100 000 cells to prevent runaway responses — split absurd ranges (`ZZ999999:AAA1` etc.) into smaller bands. | `--drive-id`, `--item-id`, `--worksheet-id`, `--address` | `GET /drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/range(address='{address}')` |
55
- | `get-excel-table` | Get the metadata (style, header row, total row) of a single named Excel table. | `--drive-id`, `--item-id`, `--table-id` | `GET /drives/{drive-id}/items/{item-id}/workbook/tables/{table-id}` |
56
- | `get-excel-used-range` | Return the worksheet's used range — the bounding box of every non-empty cell — as a single Excel range. Avoids fetching the entire 1M × 16K-cell sheet when only a small data island is populated. | `--drive-id`, `--item-id`, `--worksheet-id` | `GET /drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/usedRange()` |
57
- | `list-excel-comments` | List the modern threaded comments anchored to cells in an Excel workbook (the New Comments feature, distinct from legacy notes). Each `workbookComment` has `content`, `contentType`, `task` state, plus replies via the comment's `replies` navigation. | `--drive-id`, `--item-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/workbook/comments` |
58
- | `list-excel-defined-names` | List the workbook's defined names (named ranges, named formulas, named constants). Each `workbookNamedItem` has `name`, `value` (the formula or address), `comment`, and `scope` (workbook or worksheet). Useful for understanding workbook structure before reading ranges. | `--drive-id`, `--item-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/workbook/names` |
59
- | `list-excel-table-rows` | List the data rows of a named Excel table (excluding the header row). Note: Graph silently ignores `$filter` and `$orderby` on this endpoint, so the CLI does not expose those flags — slice / sort client-side. | `--drive-id`, `--item-id`, `--table-id`, `--top`, `--skip`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/workbook/tables/{table-id}/rows` |
60
- | `list-excel-tables` | List the named tables across every worksheet in an Excel workbook. Note: Graph silently ignores `$filter` and `$orderby` on this endpoint, so the CLI does not expose those flags — slice / sort client-side. | `--drive-id`, `--item-id`, `--top`, `--skip`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/workbook/tables` |
61
- | `list-excel-worksheet-charts` | List the charts on a worksheet. Each `workbookChart` has `id`, `name`, `height`, `width`, `top`, `left`. Use the chart's image endpoint (`.../charts/{id}/image()`) to render the chart as a base64 PNG. | `--drive-id`, `--item-id`, `--worksheet-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/charts` |
62
- | `list-excel-worksheet-pivot-tables` | List the pivot tables on a worksheet. Each `workbookPivotTable` has `name` and a navigation to its source `workbookWorksheet`. Useful for understanding analytical structure inside a workbook. | `--drive-id`, `--item-id`, `--worksheet-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/pivotTables` |
63
- | `list-excel-worksheets` | List the worksheets (tabs) inside an Excel workbook stored in OneDrive / SharePoint. Returns a clear "not an accessible Excel workbook" error if the item is a folder, non-.xlsx file, or sensitivity-label-blocked. Note: Graph silently ignores `$top`, `$filter`, and `$orderby` on this endpoint, so the CLI does not expose those flags — slice / sort client-side. | `--drive-id`, `--item-id`, `--skip`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/workbook/worksheets` |
64
-
65
- ### SharePoint Sites
66
-
67
- | Command | Description | Required params | Graph endpoint |
68
- |---------|-------------|-----------------|----------------|
69
- | `get-drive-item-list-item` | Return the SharePoint listItem projection of a OneDrive / SharePoint file — exposes the file's library-defined column values (custom metadata: status, due-date, classification, taxonomy tags, etc.) which are NOT present on the plain `driveItem`. Combine with `list-sharepoint-list-columns` to interpret the column schema. | `--drive-id`, `--item-id`, `--select`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/listItem` |
70
- | `get-sharepoint-list-column` | Return a single column definition from a SharePoint list. | `--site-id`, `--list-id`, `--column-id`, `--select`, `--expand` | `GET /sites/{site-id}/lists/{list-id}/columns/{column-id}` |
71
- | `get-sharepoint-site` | Get the metadata of a single SharePoint site by its site ID. | `--site-id`, `--select`, `--expand` | `GET /sites/{site-id}` |
72
- | `get-sharepoint-site-by-path` | Resolve a SharePoint site by its hostname + server-relative path. Use this when you have a SharePoint URL (e.g. `https://contoso.sharepoint.com/sites/Marketing`) but no site ID. | `--hostname`, `--path` | `GET /sites/{hostname}:{path}` |
73
- | `get-sharepoint-site-drive-by-id` | Get the metadata of a single document library (drive) on a SharePoint site by drive ID. | `--site-id`, `--drive-id`, `--select`, `--expand` | `GET /sites/{site-id}/drives/{drive-id}` |
74
- | `get-sharepoint-site-list` | Get the metadata (display name, template, columns) of a single SharePoint list. | `--site-id`, `--list-id`, `--select`, `--expand` | `GET /sites/{site-id}/lists/{list-id}` |
75
- | `get-sharepoint-site-list-item` | Get a single row (listItem) of a SharePoint list by ID. | `--site-id`, `--list-id`, `--list-item-id`, `--select`, `--expand` | `GET /sites/{site-id}/lists/{list-id}/items/{list-item-id}` |
76
- | `get-site-analytics` | Return view / activity analytics for a SharePoint site — `allTime` totals (visits, viewers) and `lastSevenDays` rollup. Site-level parallel to `get-drive-item-analytics`. Useful for ranking sites by attention or detecting stale workspaces. **Known empty case**: returns `{ allTime: null, lastSevenDays: null }` even on active sites when the calling identity (the Teams web client basic token) lacks the analytics scope. Do not interpret nulls as "no activity" — interpret as "not available for this caller". | `--site-id` | `GET /sites/{site-id}/analytics` |
77
- | `list-sharepoint-list-columns` | List the column definitions (schema) of a SharePoint list. Useful before reading list items so you know which fields exist and their types. Note: Graph silently ignores `$top` and `$skip` on this endpoint, so the CLI exposes only `--select` and `--expand`. | `--site-id`, `--list-id`, `--select`, `--expand` | `GET /sites/{site-id}/lists/{list-id}/columns` |
78
- | `list-sharepoint-list-item-versions` | List the version history of a SharePoint list item — every change (column edits, status flips, custom-field changes) tracked as a `listItemVersion`. Distinct from `list-drive-item-versions`, which tracks file content versions. | `--site-id`, `--list-id`, `--list-item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/lists/{list-id}/items/{list-item-id}/versions` |
79
- | `list-sharepoint-site-drives` | List the document libraries (drives) attached to a SharePoint site. | `--site-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/drives` |
80
- | `list-sharepoint-site-list-items` | List the rows (listItem resources) of a single SharePoint list. | `--site-id`, `--list-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/lists/{list-id}/items` |
81
- | `list-sharepoint-site-lists` | List all SharePoint lists (custom + built-in document libraries) on a site. Note: the skip flag is intentionally omitted — Graph rejects $skip on this endpoint with invalidRequest. Paginate via the top-level `nextLink` → `next-page`. Heads-up: when `top` is small, the FIRST page may legitimately be empty (`value: []`) while still carrying a `nextLink` — Graph filters server-side after slicing. Always check `nextLink` before concluding "no lists". | `--site-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/lists` |
82
- | `list-sharepoint-site-pages` | List modern SharePoint pages on a site (news posts, dashboards, landing pages). Each `sitePage` has `title`, `description`, `webUrl`, `publishingState`, `lastPublishedDateTime`. Returned items are the read-only listing — fetch the page body via the SharePoint REST API or by opening the `webUrl`. | `--site-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/pages` |
83
- | `list-site-columns` | List the *site-level* column definitions — columns reusable across multiple lists in the site. Distinct from `list-sharepoint-list-columns` which returns one specific list's schema. Note: Graph silently ignores `$top` and `$skip` on this endpoint (verified live — passing them returns the full collection regardless), so the CLI exposes only `--select` and `--expand`. | `--site-id`, `--select`, `--expand` | `GET /sites/{site-id}/columns` |
84
- | `list-site-content-types` | List the content type definitions of a SharePoint site — typed schemas (Document, Page, Item, custom-defined) describing which columns + behaviors apply to items of each type. Useful for understanding a site's information architecture. | `--site-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/contentTypes` |
85
- | `search-sharepoint-sites-by-name` | Search the tenant for SharePoint sites whose display name or description matches a free-text query (returns up to 25). | `--query`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites?search={query}` |
86
-
87
- ### Tasks (To Do + Planner)
88
-
89
- | Command | Description | Required params | Graph endpoint |
90
- |---------|-------------|-----------------|----------------|
91
- | `get-planner-bucket` | Get the metadata of a single Microsoft Planner bucket (column / lane). | `--planner-bucket-id` | `GET /planner/buckets/{planner-bucket-id}` |
92
- | `get-planner-plan` | Get the metadata of a single Microsoft Planner plan (title, owner group, container). | `--planner-plan-id` | `GET /planner/plans/{planner-plan-id}` |
93
- | `get-planner-task` | Get the metadata of a single Microsoft Planner task (title, assignees, dates, completion). | `--planner-task-id` | `GET /planner/tasks/{planner-task-id}` |
94
- | `get-planner-task-details` | Get the rich details (description, checklist, references) of a Microsoft Planner task. | `--planner-task-id` | `GET /planner/tasks/{planner-task-id}/details` |
95
- | `get-todo-task` | Get a single Microsoft To Do task by its ID and its parent list ID. Use `--select` to slim the response (e.g. `--select id,title,status`) or `--expand checklistItems` / `--expand linkedResources` to inline child collections. | `--todo-task-list-id`, `--todo-task-id`, `--select`, `--expand` | `GET /me/todo/lists/{todo-task-list-id}/tasks/{todo-task-id}` |
96
- | `list-incomplete-planner-tasks` | List every incomplete Microsoft Planner task assigned to or owned by the signed-in user, across every plan. Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the completion-percent predicate, and Graph rejects two `$filter` query params. If you supply `--filter` anyway, the CLI returns a clear pointer to `list-planner-tasks`. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/planner/tasks?$filter=percentComplete ne 100` |
97
- | `list-incomplete-todo-tasks` | List every incomplete Microsoft To Do task in a given list (status not equal to `completed`). Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the completion-status predicate, and Graph rejects two `$filter` query params. If you supply `--filter` anyway, the CLI returns a clear pointer to `list-todo-tasks` (which lets you AND your predicate with the completion filter yourself). | `--todo-task-list-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/todo/lists/{todo-task-list-id}/tasks?$filter=status ne 'completed'` |
98
- | `list-plan-buckets` | List the buckets (columns / lanes) of a Microsoft Planner plan. Note: Graph silently drops `$top`, `$skip`, `$filter`, and `$orderby` on this endpoint, so the CLI advertises only `--select` — slice / sort client-side. | `--planner-plan-id`, `--select` | `GET /planner/plans/{planner-plan-id}/buckets` |
99
- | `list-plan-tasks` | List every task within a Microsoft Planner plan, regardless of completion status (Graph orders by `orderHint`). Use `list-incomplete-planner-tasks` for the across-plans incomplete view. Note: Graph silently ignores standard OData query parameters on `/planner/plans/{id}/tasks` (`$top` returns the full set anyway), so the OData passthrough is intentionally NOT exposed — pipe the response through `jq` to slice client-side. | `--planner-plan-id` | `GET /planner/plans/{planner-plan-id}/tasks` |
100
- | `list-planner-plans` | List every Microsoft Planner plan the signed-in user has access to (across every group). Use this to discover plan IDs without needing an existing task as the entry point. Note: Graph silently drops `$top`, `$skip`, `$filter`, and `$orderby` on this endpoint, so the CLI advertises only `--select` — slice / sort client-side. | `--select` | `GET /me/planner/plans` |
101
- | `list-planner-tasks` | List every Microsoft Planner task assigned to or owned by the signed-in user, across all plans. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/planner/tasks` |
102
- | `list-todo-linked-resources` | List the linked resources (URLs, emails, files) attached to a Microsoft To Do task. | `--todo-task-list-id`, `--todo-task-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/todo/lists/{todo-task-list-id}/tasks/{todo-task-id}/linkedResources` |
103
- | `list-todo-task-lists` | List the signed-in user's Microsoft To Do task lists (e.g. `Tasks`, `Flagged Emails`, custom lists). Note: Graph rejects `$select` and `$orderby` on this endpoint with `RequestBroker--ParseUri`, so the CLI does not expose those flags — slice / sort client-side. | `--top`, `--skip`, `--filter`, `--expand` | `GET /me/todo/lists` |
104
- | `list-todo-tasks` | List every task in a single Microsoft To Do task list, regardless of completion status. Use `list-incomplete-todo-tasks` if you only want the open ones. Known Graph quirk: certain `--select` combinations (notably any combo that includes `title`) trip `RequestBroker--ParseUri` on this endpoint; the CLI rewrites that opaque error to a hint pointing at the workaround. | `--todo-task-list-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/todo/lists/{todo-task-list-id}/tasks` |
105
- | `list-todo-tasks-delta` | Track incremental task changes (added / updated / completed / deleted) within a single Microsoft To Do list. The first call returns the current snapshot plus `@odata.deltaLink`; subsequent calls with that link return only what has changed since. Note: Graph rejects standard OData query parameters on this delta endpoint (the page-cap flag throws `Skip token is not provided`), so the OData passthrough is intentionally NOT exposed here. Use `next-page` with the returned `@odata.nextLink` to walk pages. | `--todo-task-list-id` | `GET /me/todo/lists/{todo-task-list-id}/tasks/delta()` |
106
-
107
- ### Mail
108
-
109
- | Command | Description | Required params | Graph endpoint |
110
- |---------|-------------|-----------------|----------------|
111
- | `convert-mail-attachment-to-markdown` | Convert an Outlook mail attachment to markdown. Polymorphic on the attachment’s `@odata.type`: fileAttachment decodes the inline bytes and runs them through the local conversion pipeline (docx via mammoth, xlsx via sheetjs, csv as markdown table, plus plain-text passthrough); referenceAttachment resolves via /shares/{token}/driveItem and routes through the same dispatcher; itemAttachment (embedded mail / event / contact) is rendered locally via dedicated renderers. For pptx attachments, `convert-mail-attachment-to-pdf` is recommended (Graph PDF preserves slide layout). For pdf/rtf/odt/etc. also use the PDF sibling. Loop/Fluid/Whiteboard reference-attachments use Graph `?format=html` (the four inputs Microsoft documents). | `--message-id`, `--attachment-id` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
112
- | `convert-mail-attachment-to-pdf` | Convert an Outlook mail attachment to PDF on the fly. Polymorphic on the attachment’s `@odata.type`: fileAttachment uploads the bytes to a temp folder under /me/drive (large files use Graph’s chunked upload session — no 4 MB ceiling), runs ?format=pdf, then deletes the temp item; referenceAttachment resolves via /shares/{token}/driveItem and runs ?format=pdf in place; plain-text source extensions and `pdf` sources short-circuit to a raw-bytes envelope on either path (Graph’s `?format=pdf` does not accept `pdf` as an input format — pdf attachments are returned as-is). itemAttachment (embedded mail/event/contact) is unsupported here — Graph rejects those source types — use convert-mail-attachment-to-markdown instead. Worst-case wall-clock for huge attachments is ~22 minutes (1 metadata GET + up-to-20 chunk PUTs + 1 convert GET + 1 cleanup DELETE, each capped at 60s). | `--message-id`, `--attachment-id` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
113
- | `convert-mail-to-markdown` | Render a single Outlook email as markdown — headers (`**Subject:**`, `**From:**`, `**To:**`, `**Cc:**` only when present, `**Date:**`), followed by the body run through turndown. Inline images attached with `isInline:true` and an `image/*` content-type (size ≤ 2 MB) are embedded as base64 `data:` URIs so the output is self-contained (Hardening #1: non-image inline attachments are NOT embedded; oversize inline images are replaced with a placeholder note). File attachments are listed below the body by name + size + id; their bytes are NOT fetched here — call `convert-mail-attachment-to-pdf` or `get-mail-attachment` with the id when you actually need them. Staged-fetch design (audit v1.0.0): one call for the body, one for the attachments-metadata list (only if `hasAttachments:true`), and one per small inline image — replaces the old `?$expand=attachments` which timed out / truncated on messages with multi-MB attachments. | `--message-id` | `GET /me/messages/{message-id}` |
114
- | `extract-sharepoint-links-in-mail` | Find every `*.sharepoint.com` URL in the body of a single Outlook email and resolve each one to its driveItem (driveId, itemId, name, webUrl) so the agent can feed those into `download-drive-item-as-pdf` / `-as-markdown` etc. Read-only — no conversion happens here. Capped at 25 unique URLs per call to bound fan-out (returns `truncated: true` and `skippedCount` when the body has more); duplicate URLs are deduplicated. Per-link errors are captured inside each entry instead of failing the whole call. | `--message-id` | `GET /me/messages/{message-id}` |
115
- | `get-mail-attachment` | Get a single attachment on an Outlook message (metadata, plus the base64 `contentBytes` for file attachments). For fileAttachments, the response also carries a `base64` mirror of `contentBytes` so the global output-path flag can land the bytes on disk in one call. Use `--select id,name,contentType,size` to fetch metadata only and skip the multi-MB `contentBytes` payload. | `--message-id`, `--attachment-id`, `--select`, `--expand` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
116
- | `get-mail-message` | Get a single Outlook message by ID, including subject, sender, body, and flags. Pass `--select id,subject,from,receivedDateTime` to fetch only the fields the LLM needs (a full message body can be 50+ KB; the audit found this swelling LLM context unnecessarily). | `--message-id`, `--select`, `--expand` | `GET /me/messages/{message-id}` |
117
- | `get-mail-message-mime` | Return the raw RFC 5322 MIME source of a single Outlook message — full headers, every attachment encoded inline. Useful for archiving, full-fidelity forensic inspection, or feeding into a tool that reads MIME directly. For human-readable content prefer `get-mail-message` or `convert-mail-to-markdown`. | `--message-id` | `GET /me/messages/{message-id}/$value` |
118
- | `get-mail-rule` | Return a single Outlook message rule by ID, including its conditions and actions. Sibling to `list-mail-rules`. `--mail-folder-id` defaults to `inbox` (the only folder where rules actually live in Graph); the flag is preserved for callers that want to pass a resolved Inbox ID explicitly. | `--mail-folder-id`, `--message-rule-id` | `GET /me/mailFolders/{mail-folder-id}/messageRules/{message-rule-id}` |
119
- | `get-mailbox-settings` | Get the signed-in user's Outlook mailbox settings (timezone, working hours, automatic replies). Note: Graph silently ignores `$select` / `$expand` on this endpoint, so the CLI does NOT expose them — the full payload (including the auto-reply HTML body) is always returned. Slim client-side if you only need a subset. | _(none)_ | `GET /me/mailboxSettings` |
120
- | `get-shared-mailbox-message` | Return a single message from a shared / delegated mailbox. Use `--select` to fetch only specific fields (e.g. `--select id,subject,from,receivedDateTime`) — sibling to `get-mail-message` for /me. | `--user-id`, `--message-id`, `--select`, `--expand` | `GET /users/{user-id}/messages/{message-id}` |
121
- | `list-conversation-messages` | List every message in a single Outlook conversation (thread) using `$filter=conversationId eq '...'`. Reconstructs a complete thread regardless of which subject lines or folders the replies landed in. Accepts the OData passthrough flags top/skip/select/expand — the filter and orderby passthroughs are intentionally omitted (the path already pins a `$filter`, and Graph rejects this filter combined with `$orderby` as `InefficientFilter` since `conversationId` is not a sortable index). The caller can sort by `receivedDateTime` client-side. KQL `$search` does not index `conversationId`, so `$filter` is the only documented Graph idiom for whole-thread retrieval. | `--conversation-id`, `--top`, `--skip`, `--select`, `--expand` | `GET /me/messages?$filter=conversationId eq '{conversation-id}'` |
122
- | `list-focused-inbox-overrides` | List the signed-in user's Focused Inbox classification overrides — sender addresses they've manually moved to Focused or Other, which override Microsoft's automatic classifier. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/inferenceClassification/overrides` |
123
- | `list-group-conversations` | List conversations in a unified (Microsoft 365) group inbox. Each conversation aggregates one or more threads. Only Microsoft 365 groups have a mailbox — security and distribution groups return `MailboxNotEnabledForRESTAPI`. Verify the group is unified before calling. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/conversations` |
124
- | `list-group-threads` | List threads in a unified (Microsoft 365) group inbox. Threads are flatter than conversations — one per topic, useful when conversation-level grouping isn't needed. Only Microsoft 365 groups have a mailbox — security and distribution groups return `MailboxNotEnabledForRESTAPI`. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/threads` |
125
- | `list-mail-attachments` | List the attachments (file, item, reference) on a single Outlook message. The CLI ships an opinionated default `--select=id,name,contentType,size,isInline` so an LLM that doesn't slim the response itself doesn't accidentally pull multi-MB `contentBytes` for every attachment (a single 1.5 MB image attachment would otherwise blow the context window). The `@odata.type` discriminator is always returned by Graph regardless of `$select` (and Graph rejects asking for it explicitly). To fetch the actual bytes, call `get-mail-attachment` for the one you need (or override `--select` if you really want the raw inline payload). | `--message-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages/{message-id}/attachments` |
126
- | `list-mail-child-folders` | List the subfolders of a single Outlook mail folder (e.g. subfolders of Inbox). | `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/childFolders` |
127
- | `list-mail-folder-messages` | List the messages inside a specific Outlook mail folder (Inbox, custom folder, etc.). | `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/messages` |
128
- | `list-mail-folder-messages-delta` | Track incremental changes (added / updated / deleted messages) within a single mail folder using Microsoft Graph delta tokens. The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed since. | `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/messages/delta()` |
129
- | `list-mail-folders` | List the top-level mail folders in the signed-in user’s Outlook mailbox (Inbox, Sent Items, etc.). | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders` |
130
- | `list-mail-folders-delta` | Track incremental changes to the mail-folder tree itself (folders added / renamed / deleted). The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed. Companion to `list-mail-folder-messages-delta` which tracks message changes inside one folder. Note: Graph explicitly rejects `$top`, `$filter`, `$orderby`, and `$search` on this delta endpoint (`ErrorInvalidUrlQuery: not supported with change tracking over the 'Folders' resource`), so the OData passthrough is intentionally NOT exposed here. | _(none)_ | `GET /me/mailFolders/delta()` |
131
- | `list-mail-messages` | List the most recent messages from across the signed-in user’s entire Outlook mailbox (every folder including Sent, Archive, Junk; default sort `receivedDateTime` desc). Use `list-mail-folder-messages` to scope to a single folder such as Inbox. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages` |
132
- | `list-mail-rules` | List the message rules on the Outlook Inbox. Microsoft Graph only supports message rules on the Inbox folder; passing any other folder ID (drafts, sentitems, archive, a custom folder) returns `MailFolderNotSupportedError` from Graph. `--mail-folder-id` defaults to `inbox` because that is the only value Graph accepts; the flag is kept (optional) for callers that want to pass a resolved Inbox ID explicitly. Note: Graph silently ignores every OData passthrough on this endpoint, so the CLI does NOT expose them — the full rule set is always returned. | `--mail-folder-id` | `GET /me/mailFolders/{mail-folder-id}/messageRules` |
133
- | `list-outlook-categories` | List the signed-in user's Outlook color categories — the named tags that can be applied to mail, calendar items, and contacts. Each entry has `displayName` and a `color` from Outlook's preset palette. Note: Graph silently ignores every OData passthrough on this endpoint (`$top`, `$skip`, `$select`, `$filter`, `$orderby`, `$expand`), so the CLI does not expose any of those flags — the full collection is always returned. Slice client-side. | _(none)_ | `GET /me/outlook/masterCategories` |
134
- | `list-shared-mailbox-folder-messages` | List messages in a single folder of a shared / delegated mailbox. | `--user-id`, `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/mailFolders/{mail-folder-id}/messages` |
135
- | `list-shared-mailbox-messages` | List messages from a shared or delegated mailbox the signed-in user has read access to. Same shape as `list-mail-messages` but scoped to a specific mailbox owner. 403 if the signed-in user does not have shared access to that mailbox. | `--user-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/messages` |
136
- | `search-mail-messages` | Search the signed-in user's entire Outlook mailbox using KQL or free text. Results are ranked by Graph relevance. Note: Graph does not allow `$search` and `$filter` together — the CLI rejects `--filter` client-side with a pointer to `list-mail-messages` (which supports OData filtering). For sorting, server-side `$orderby` is also not allowed with `$search`; use the relevance ranking Graph returns. | `--query`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages?$search="{query}"` |
137
-
138
- ### Notes (OneNote)
139
-
140
- | Command | Description | Required params | Graph endpoint |
141
- |---------|-------------|-----------------|----------------|
142
- | `get-onenote-page-as-markdown` | Get the body of a single OneNote page as markdown. Graph already returns OneNote pages as HTML, so this command runs that HTML through turndown locally. Inline image references in the page survive as Graph resource URLs (they are NOT base64-embedded — that is future work). For the raw HTML use `get-onenote-page-content`. | `--onenote-page-id` | `GET /me/onenote/pages/{onenote-page-id}/content` |
143
- | `get-onenote-page-content` | Get the raw HTML body of a single OneNote page. Returned as a `text/html` payload so the HTML body is available verbatim (text mode prints the body raw; JSON mode wraps it in the standard `{contentType, size, text}` envelope). For markdown output use `get-onenote-page-as-markdown`. | `--onenote-page-id` | `GET /me/onenote/pages/{onenote-page-id}/content` |
144
- | `get-sharepoint-site-onenote-page-content` | Return the HTML content of a single OneNote page from a SharePoint site (parallel to `get-onenote-page-content` for `/me`). The response carries the standard `{contentType: text/html, size, text}` shape so the HTML body is available verbatim under either output format. | `--site-id`, `--onenote-page-id` | `GET /sites/{site-id}/onenote/pages/{onenote-page-id}/content` |
145
- | `list-all-onenote-sections` | List every OneNote section the signed-in user can see, across all notebooks. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/onenote/sections` |
146
- | `list-onenote-notebook-sections` | List the top-level sections of a single OneNote notebook (flat — does NOT recurse into section groups; use `list-all-onenote-sections` to flatten every notebook the user has access to). | `--notebook-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/onenote/notebooks/{notebook-id}/sections` |
147
- | `list-onenote-notebooks` | List the OneNote notebooks the signed-in user owns or has access to (sorted by `createdDateTime` desc by Graph; soft-deleted notebooks excluded). | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/onenote/notebooks` |
148
- | `list-onenote-section-pages` | List the pages inside a single OneNote section. | `--onenote-section-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/onenote/sections/{onenote-section-id}/pages` |
149
- | `list-sharepoint-site-onenote-notebook-sections` | List sections inside one OneNote notebook attached to a SharePoint site. | `--site-id`, `--notebook-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/onenote/notebooks/{notebook-id}/sections` |
150
- | `list-sharepoint-site-onenote-notebooks` | List OneNote notebooks attached to a SharePoint site (separate from the personal `list-onenote-notebooks` which targets `/me`). | `--site-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/onenote/notebooks` |
151
- | `list-sharepoint-site-onenote-section-pages` | List pages inside one section of a SharePoint-site OneNote notebook. | `--site-id`, `--onenote-section-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /sites/{site-id}/onenote/sections/{onenote-section-id}/pages` |
152
- | `search-onenote-pages` | Find OneNote pages whose title contains a substring (case-sensitive — page content is NOT searched). Microsoft removed full-text OneNote `?search=` from v1.0 Graph; only $filter against `title` remains, which is what this command runs. Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the title-contains predicate, and Graph rejects two `$filter` query params. | `--title-substring`, `--top`, `--skip`, `--select`, `--orderby`, `--expand` | `GET /me/onenote/pages?$filter=contains(title,'{title-substring}')` |
153
-
154
- ### User
155
-
156
- | Command | Description | Required params | Graph endpoint |
157
- |---------|-------------|-----------------|----------------|
158
- | `get-current-user` | Return the signed-in user’s Microsoft Graph profile (id, displayName, mail, jobTitle, etc.). Pass `--select id,displayName,mail` to slim the payload to just the fields you need. | `--select`, `--expand` | `GET /me` |
159
- | `get-group` | Return metadata for a single Azure AD / Microsoft 365 group. Use `--select` to slim large group payloads (the full group resource includes 30+ fields). | `--group-id`, `--select`, `--expand` | `GET /groups/{group-id}` |
160
- | `get-my-manager` | Return the signed-in user's manager (a single `user` resource). When no manager is set in the directory, Graph returns 404 `Request_ResourceNotFound`; this command maps that one specific 404 to `{ ok: true, data: { manager: null, note: '...' } }` so an LLM can distinguish 'no manager' from a permission failure without parsing prose. Use `--select` to slim the response (e.g. `--select id,displayName,mail`). | `--select`, `--expand` | `GET /me/manager` |
161
- | `get-my-profile-photo` | Download the signed-in user's profile photo (largest available size), inlined. The CLI follows the Graph 302 → CDN redirect internally so the LLM never has to fetch an external URL. | _(none)_ | `GET /me/photo/$value` |
162
- | `get-organization` | Return the tenant's organization metadata — display name, country, verified domains, business phones, technical / security notification contacts, assigned Microsoft 365 SKUs / licensing. Graph wraps the single organization resource under `value[]` (audit v1.0.0 §D7 — even though only one tenant exists, the endpoint returns a collection). The full resource is ~57 KB; use `--select` to slim it (e.g. `--select id,displayName,verifiedDomains`). | `--select`, `--expand` | `GET /organization` |
163
- | `get-user-manager` | Return a specific user's manager (a single `user` resource). When the user has no manager set in the directory, Graph returns 404 `Request_ResourceNotFound`; this command maps that one specific 404 to `{ ok: true, data: { manager: null, note: '...' } }` (same shape as `get-my-manager`) so an LLM can distinguish 'no manager' from 'unknown user' with a single discriminator across both commands. Use `--select` to slim the response. | `--user-id`, `--select`, `--expand` | `GET /users/{user-id}/manager` |
164
- | `list-group-members` | List members of an Azure AD / Microsoft 365 group. Returns users, groups, and other directoryObjects depending on the group's membership. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/members` |
165
- | `list-group-owners` | List the owners of an Azure AD / Microsoft 365 group. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/owners` |
166
- | `list-groups` | List Microsoft 365 groups, security groups, and distribution groups in the tenant directory. Use `--top` and `next-page` to paginate over very large directories. | `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups` |
167
- | `list-my-direct-reports` | List the signed-in user's direct reports (employees who report to them in the directory). When `--orderby` is supplied the CLI auto-injects the `ConsistencyLevel: eventual` header Graph requires on directory endpoints — otherwise Graph rejects the sort with `Request_UnsupportedQuery`. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/directReports` |
168
- | `list-my-memberships` | List the groups, directory roles, and administrative units the signed-in user is a member of. Each entry's `@odata.type` distinguishes #microsoft.graph.group from #microsoft.graph.directoryRole, etc. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/memberOf` |
169
- | `list-my-transitive-memberships` | List all groups, directory roles, and administrative units the signed-in user is a member of *transitively* — including memberships inherited via nested groups. Sibling to `list-my-memberships` (`/me/memberOf`) which only returns direct memberships. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/transitiveMemberOf` |
170
- | `list-relevant-people` | List people relevant to the signed-in user — colleagues they email and meet with most. Microsoft's relevance ranking, not the full directory. Returns `displayName`, `emailAddresses`, `jobTitle`, `companyName`, etc. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/people` |
171
- | `list-sensitivity-labels` | List the Microsoft Information Protection sensitivity labels available to the signed-in user — the labels Outlook / Word / SharePoint surfaces in the "Sensitivity" picker (e.g. Public / Internal / Confidential / Highly Confidential). Each label has `id`, `displayName`, `priority`, `isAppliable`, `tooltip`. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/informationProtection/sensitivityLabels` |
172
- | `list-user-direct-reports` | List a specific user's direct reports. | `--user-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/directReports` |
173
-
174
- ### Calendar
175
-
176
- | Command | Description | Required params | Graph endpoint |
177
- |---------|-------------|-----------------|----------------|
178
- | `get-calendar-event` | Fetch a single calendar event by ID from the signed-in user’s default calendar. Pass `--select` to project only the fields you need (the full event body can be large with HTML body and attendee lists). | `--event-id`, `--select`, `--expand` | `GET /me/events/{event-id}` |
179
- | `get-my-calendar` | Return metadata for the signed-in user's *primary* calendar — `id`, `name`, `color`, `owner`, `canShare`, `canViewPrivateItems`, `canEdit`, `defaultOnlineMeetingProvider`. Sibling to `list-calendars` which returns every calendar (incl. shared / subscribed). Use `--select` to fetch only the fields you need. | `--select`, `--expand` | `GET /me/calendar` |
180
- | `get-specific-calendar-event` | Fetch a single calendar event by ID from a specific calendar. `--calendar-id primary` (or `default`) targets the signed-in user's default calendar. Use `--select` to slim large event payloads (a typical event with body+attendees runs >50 KB). | `--calendar-id`, `--event-id`, `--select`, `--expand` | `GET /me/calendars/{calendar-id}/events/{event-id}` |
181
- | `list-calendar-event-instances` | List the individual occurrences of a recurring calendar event over a date range. Both ISO date-time params are required by Graph. `--calendar-id` is optional and defaults to `primary` (the signed-in user’s default calendar) — most callers know the event-id but not which calendar it lives in. Pass an explicit `--calendar-id` only when targeting a non-default calendar. | `--calendar-id`, `--event-id`, `--start-date-time`, `--end-date-time`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendars/{calendar-id}/events/{event-id}/instances?startDateTime={start-date-time}&endDateTime={end-date-time}` |
182
- | `list-calendar-events` | List the events in the signed-in user’s default calendar (does not expand recurrences). | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/events` |
183
- | `list-calendar-events-delta` | Get the incremental change set (added / modified / deleted events) for the signed-in user's default calendar. Use the `@odata.deltaLink` from a previous response to resume. The CLI translates `--top` into the `Prefer: odata.maxpagesize=N` header internally; `$top` as a URL query is rejected by Graph (`ErrorInvalidUrlQuery`). Other OData passthroughs (`$select`, `$filter`, `$orderby`, `$skip`) are silently ignored by Graph on this delta endpoint, so the CLI does NOT expose them — slice / sort / project client-side. Most tenants accept the call without `--top` and return a sane page (~200 events); pass `--top` only when you want a smaller bound. If Graph returns an empty `UnknownError:` (rare), the CLI rewrites it to a hint pointing at the `--top` workaround. | `--top` | `GET /me/events/delta()` |
184
- | `list-calendar-group-calendars` | List the calendars inside one calendar group. | `--calendar-group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendarGroups/{calendar-group-id}/calendars` |
185
- | `list-calendar-groups` | List the signed-in user's calendar groups — Outlook's organizational layer above individual calendars (e.g. "My Calendars", "Other Calendars", "Birthdays"). Use the returned `id` with `list-calendar-group-calendars` to drill in. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendarGroups` |
186
- | `list-calendar-view` | List the signed-in user’s default-calendar events with recurrence expanded into individual occurrences in a date range. Both ISO date-time params are required by Graph. | `--start-date-time`, `--end-date-time`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}` |
187
- | `list-calendar-view-delta` | Get the first page of the incremental change set of expanded calendar-view occurrences over a date range. Subsequent pages: feed the returned `@odata.nextLink` to `next-page`; resume later via the `@odata.deltaLink`. The CLI translates `--top` into the `Prefer: odata.maxpagesize=N` header internally — `$top` as a URL query is rejected by Graph (`ErrorInvalidUrlQuery`). Other OData passthroughs (`$select`, `$filter`, `$orderby`, `$skip`) are silently ignored by Graph on this delta endpoint, so the CLI does NOT expose them. | `--start-date-time`, `--end-date-time`, `--top` | `GET /me/calendarView/delta()?startDateTime={start-date-time}&endDateTime={end-date-time}` |
188
- | `list-calendars` | List the calendars in the signed-in user’s mailbox (default + secondary calendars + shared calendars). | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendars` |
189
- | `list-group-calendar-view` | Return a date-windowed calendar view from a unified (Microsoft 365) group's calendar. Recurring events are expanded into individual occurrences across the window. Only Microsoft 365 groups have a calendar — security and distribution groups return `MailboxNotEnabledForRESTAPI`. | `--group-id`, `--start-date-time`, `--end-date-time`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}` |
190
- | `list-group-events` | List events from a unified (Microsoft 365) group's calendar. Only Microsoft 365 groups have a calendar — security and distribution groups return an empty `value[]` or 404. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/events` |
191
- | `list-room-lists` | List room lists — usually one per building. Use these to scope a room search by location: a roomList groups the rooms in one office, then `/places/{roomList}/rooms` lists just those rooms. Pass `--top N` to limit the response on large tenants. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /places/microsoft.graph.roomList` |
192
- | `list-rooms` | List bookable meeting rooms in the tenant. Each `room` has `displayName`, `emailAddress`, `capacity`, `building`, `floorNumber`, and `isWheelChairAccessible`. Use the `emailAddress` as a meeting `attendee` for room booking. Pass `--top 5` to limit the response — large tenants return tens of KB by default. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /places/microsoft.graph.room` |
193
- | `list-shared-calendar-events` | List events from another user's primary calendar (shared / delegated access). 403 without `Calendars.Read.Shared`. | `--user-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/calendar/events` |
194
- | `list-shared-calendar-view` | Return a date-windowed calendar view from another user's primary calendar (shared / delegated access). Recurrences expanded into individual occurrences. | `--user-id`, `--start-date-time`, `--end-date-time`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}` |
195
- | `list-specific-calendar-events` | List the events in a specific calendar (does not expand recurrences). `--calendar-id primary` (or `default`) routes to the signed-in user’s default calendar (`/me/calendar/events`); any other value goes to `/me/calendars/{id}/events` and must be a real calendar ID. | `--calendar-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendars/{calendar-id}/events` |
196
- | `list-specific-calendar-view` | List the events in a specific calendar with recurrence expanded into individual occurrences in a date range. Both ISO date-time params are required by Graph. `--calendar-id primary` (or `default`) routes to the signed-in user’s default calendar. | `--calendar-id`, `--start-date-time`, `--end-date-time`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendars/{calendar-id}/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}` |
197
-
198
- ### Chats
199
-
200
- | Command | Description | Required params | Graph endpoint |
201
- |---------|-------------|-----------------|----------------|
202
- | `get-chat` | Return metadata for a single Microsoft Teams chat (1:1, group, or meeting). Returns `id`, `topic`, `chatType`, `lastUpdatedDateTime`, etc. — not the messages (which need `Chat.Read*`). Requires the M365ChatClient elevated token captured at login (the basic Teams web client token lacks `Chat.ReadBasic`). | `--chat-id` | `GET /chats/{chat-id}` |
203
- | `list-chat-members` | List the members of a single Microsoft Teams chat. Graph rejects `$top` / `$orderby` / `$expand` on this endpoint, so the CLI advertises only the subset Graph honours (`--skip`, `--select`, `--filter`). | `--chat-id`, `--skip`, `--select`, `--filter` | `GET /chats/{chat-id}/members` |
204
- | `list-chats` | List the signed-in user's Microsoft Teams chats (1:1, group, and meeting chats). Returns chat metadata only — `id`, `topic`, `chatType`, `lastUpdatedDateTime`, etc. Reading chat *messages* needs `Chat.Read*` which neither token grants. Requires the M365ChatClient elevated token captured at login (the basic Teams web client token lacks `Chat.ReadBasic`). Graph rejects `$orderby` and hangs on `$expand` for this endpoint, so the CLI advertises only the subset Graph honours (`--top`, `--skip`, `--select`, `--filter`). | `--top`, `--skip`, `--select`, `--filter` | `GET /me/chats` |
205
-
206
- ### Teams
207
-
208
- | Command | Description | Required params | Graph endpoint |
209
- |---------|-------------|-----------------|----------------|
210
- | `get-channel-files-folder` | Return the SharePoint folder that backs a Teams channel's Files tab. Returned `driveItem` includes `parentReference.driveId` and `id` so you can pivot into `list-folder-files`, `download-onedrive-file-content`, etc., and treat the channel like any other OneDrive folder. Requires that the signed-in user is a member of the channel — restricted channels return `AccessDenied`. | `--team-id`, `--channel-id`, `--select`, `--expand` | `GET /teams/{team-id}/channels/{channel-id}/filesFolder` |
211
- | `get-team` | Get the metadata of a single Microsoft Team (display name, settings, member-settings, owner group). Pass `--select displayName,description,visibility` to slim the response. | `--team-id`, `--select`, `--expand` | `GET /teams/{team-id}` |
212
- | `get-team-channel` | Get the metadata of a single channel inside a Microsoft Team. Use `--select` to slim the response (e.g. `--select id,displayName,webUrl`) — sibling to `get-team` and `get-team-primary-channel` which both expose the same flag. | `--team-id`, `--channel-id`, `--select`, `--expand` | `GET /teams/{team-id}/channels/{channel-id}` |
213
- | `get-team-primary-channel` | Return the team's primary (General) channel directly without having to list-then-pick. The returned `channel` has `id`, `displayName`, `webUrl`, `email` — feed `id` into `list-team-channels` siblings or `get-channel-files-folder`. | `--team-id`, `--select`, `--expand` | `GET /teams/{team-id}/primaryChannel` |
214
- | `list-joined-teams` | List the Microsoft Teams the signed-in user is a member of. Note: this endpoint does NOT accept the standard OData query parameters — Graph rejects `$top`/`$select`/`$filter`/etc. on `/me/joinedTeams` with `Query option 'X' is not allowed`. The CLI omits the OData passthrough on this command for that reason; pass post-processing through `jq` instead if you need to slice the response. | _(none)_ | `GET /me/joinedTeams` |
215
- | `list-team-channels` | List the channels (standard, private, shared) inside a single Microsoft Team. Microsoft documents this endpoint as supporting only `$filter` and `$select` — Graph returns `BadRequest` on `$top`, `$skip`, `$orderby`, `$expand`, so the CLI exposes only the two flags that actually work. | `--team-id`, `--select`, `--filter` | `GET /teams/{team-id}/channels` |
216
- | `list-team-installed-apps` | List the Teams apps installed in a team. The CLI hard-pins `$expand=teamsAppDefinition` so every entry includes `displayName`, `version`, and `distributionMethod` (the bare endpoint returns only opaque IDs). Useful for surfacing which integrations are wired into a given team. Graph rejects standard OData query parameters on this endpoint (`Query option 'Top' is not allowed`); the OData passthrough is intentionally NOT exposed. | `--team-id` | `GET /teams/{team-id}/installedApps?$expand=teamsAppDefinition` |
217
-
218
- ### Meta / Pagination
219
-
220
- | Command | Description | Required params | Graph endpoint |
221
- |---------|-------------|-----------------|----------------|
222
- | `microsoft-search-query` | Run a federated KQL search across the signed-in user's mail, files, list items, sites, calendar events, and people. Microsoft Graph v1.0 rejects multi-entity search bodies on most tenants (`Multiple entity search is not supported in v1.0`), so this command issues SIX parallel POSTs — one per entityType — and merges the per-entity `searchHits` containers into a single `value[]`. Each container is identifiable by the resource type inside `hits[].resource`. If a sub-request fails (e.g. tenant lacks the scope for one entity), the others still return; failures show up in `partialErrors[]`. Page size is fixed at 25 per sub-request and `top` is NOT exposed (Graph rejects $top in /search/query bodies). `chatMessage` is excluded since `Chat.Read*` is unavailable. | `--query` | `POST /search/query` |
223
- | `my-quick-context` | One-shot discovery for the IDs every other command needs. Issues nine Graph calls in parallel and returns the IDs each succeeded for. Partial-result mode: only `/me` is load-bearing — if any other sub-call fails (missing license, scope, or tenant policy) the corresponding field is `undefined` but the rest are still returned. Replaces the audit's 5-call discovery chain — feed the IDs straight into `list-mail-folder-messages`, `list-folder-files`, `list-todo-tasks`, `list-planner-tasks`, `list-onenote-notebook-sections`, etc. | _(none)_ | `GET (meta) parallel: /me, /me/drive, /me/mailFolders/inbox, /me/todo/lists, /me/calendar, /me/planner/plans, /me/onenote/notebooks, /me/joinedTeams, /me/drive/recent` |
224
- | `next-page` | Fetch the next page of a paginated Graph response. Pass the cursor the previous command emitted — in text mode that is the `next: <url>` value in the `---` footer; in JSON mode it is the top-level `nextLink` field. Never reach into `data["@odata.nextLink"]`; the CLI strips that and surfaces it as a first-class envelope/footer field. Automatically signs `/me/chats` and `/chats/...` cursors with the M365ChatClient elevated token to match the chat-metadata commands. | `--url` | `GET {url}` |
225
- | `scopes-check` | Decode the cached Teams web client access token and return its scopes, audience, and expiry without making a Graph call. Use this as a self-test before running a command an LLM expects to fail with `accessDenied` — if the required scope isn't in the returned list, the call will reject regardless of tenant config. Each command's `scopesRequired` field in `help-json` lists the scopes that command needs; intersect with the array returned here for a pre-flight check (pipe both through `jq` and diff). | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
226
-
227
- <!-- AUTO-GENERATED-COMMANDS:END -->
228
- ## Install
229
-
230
- Requires Node ≥20 **or** Bun ≥1.0 on the user's machine. Works on Windows, macOS, and Linux.
3
+ **A Microsoft Graph CLI built for LLMs.** 177 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.
231
4
 
232
5
  ```bash
233
- npm i -g ask-marcel-office-cli # any platform with Node
234
- # or
235
- bun add -g ask-marcel-office-cli # any platform with Bun
6
+ npm i -g ask-marcel-office-cli
7
+ ask-marcel login # browser opens once, token cached
8
+ ask-marcel my-quick-context # who am I + my IDs, in one round trip
9
+ ask-marcel list-mail-messages --top 5
236
10
  ```
237
11
 
238
- The first launch prints a one-time notice if a newer version is on npm; update with the same command above plus `@latest`.
12
+ ---
239
13
 
240
- ## Usage (CLI)
14
+ ## Why it exists
241
15
 
242
- ```bash
243
- # authenticate (cached → refresh → browser fallback)
244
- ask-marcel login
16
+ LLM tool-loops keep hitting the same three walls with Microsoft Graph:
245
17
 
246
- # list drives
247
- ask-marcel list-drives
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.
248
21
 
249
- # search for files
250
- ask-marcel search-onedrive-files --drive-id abc123 --query "report"
22
+ `ask-marcel` fixes all three at the CLI layer, so the model just calls commands and reads back-pressure-friendly responses.
251
23
 
252
- # get Excel table data
253
- ask-marcel list-excel-table-rows --drive-id abc123 --item-id xyz789 --table-id table1
24
+ ## What you get
254
25
 
255
- # search SharePoint sites by name (free-text)
256
- ask-marcel search-sharepoint-sites-by-name --query "marketing"
26
+ ### Read-only by design
257
27
 
258
- # list SharePoint site lists
259
- ask-marcel list-sharepoint-site-lists --site-id contoso.sharepoint.com,1234-5678
28
+ **This is the most important property.** 173 GET endpoints + 2 POST (searches) + 1 POST (create draft) + 1 PATCH (update draft) = 177 commands. No `send-mail`, no `create-event`, no `upload-file`, no `delete-anything`. The only write operations are draft creation 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.
260
29
 
261
- # update to the latest version (auto-detects npm vs bun)
262
- ask-marcel update
30
+ ### One call gets the full email context
263
31
 
264
- # clear tokens
265
- ask-marcel logout
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.
266
33
 
267
- # see all commands
268
- ask-marcel --help
269
- ```
34
+ `convert-mail-to-markdown` collapses that into one call:
270
35
 
271
- `ask-marcel update` auto-detects whether the CLI was installed via npm or bun (based on the bin path) and reinstalls globally with the matching tool. You can still run the install manually: `npm i -g ask-marcel-office-cli@latest` or `bun add -g ask-marcel-office-cli@latest`.
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)
272
41
 
273
- During development from a clone you can keep using `bun run src/main.ts <command>`.
42
+ ### Office docs markdown or PDF on the fly
274
43
 
275
- ### Output formats `--output text` (default) vs `--output json`
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:
276
45
 
277
- Every command writes its output as a single document to **stdout** (success or errorthere is no stderr output). `process.exitCode` is `0` on success and `1` on any failure. Pick the format with the global `--output <text|json>` flag.
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
278
54
 
279
- **Text (default, LLM-readable)** — YAML-ish `key: value` lines, ~30–60% fewer tokens than the JSON envelope on listings; errors render as `error: <message>`. Designed for LLMs reading and summarising; not for piping into other tools.
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.
280
56
 
281
- ```bash
282
- $ ask-marcel get-current-user
283
- id: 0c1d2e3f-…
284
- displayName: Vincent Delacourt
285
- mail: vincent@example.com
57
+ ### Extract embedded images from documents
286
58
 
287
- $ ask-marcel list-mail-folder-messages --mail-folder-id inbox --top 2
288
- id: AAMkAGI2…
289
- subject: Re: Q2 planning
290
- from: alice@example.com
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.
291
60
 
292
- id: AAMkAGI3…
293
- subject: Lunch?
294
- from: bob@example.com
61
+ The CLI follows any SharePoint media-transform redirect internally, so the LLM never has to fetch an external URL.
295
62
 
296
- --- next: https://graph.microsoft.com/v1.0/me/messages?$skip=2
63
+ ### Find every drive you can reach
297
64
 
298
- $ ask-marcel login # after a failure
299
- error: Authentication cancelled
300
- ```
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).
301
66
 
302
- Pagination cursors (`nextLink`, `deltaLink`) and `count` render as a single footer line prefixed with `---` and separated by middle dots, so an LLM can still walk paginated responses without parsing JSON. Empty listings render as `(no items)` so silence is never ambiguous. Binary commands (PDFs etc.) print `binary: <contentType>, <size> bytes — use --output-path to save` instead of a base64 blob.
67
+ ### Browser-OAuth at first launch
303
68
 
304
- **JSON (`--output json`, opt-in for tool-chaining)** the stable `{ok, data, nextLink?, deltaLink?, count?}` envelope, unambiguous for `jq`/script extraction and chaining one command's output into another's `--filter` / `--message-id`.
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.
305
70
 
306
- ```jsonc
307
- // Success
308
- {
309
- "ok": true,
310
- "data": { /* the Graph payload, or whatever the use-case returned */ },
311
- "nextLink": "https://graph.microsoft.com/v1.0/me/messages?$skip=10", // paginated responses
312
- "deltaLink": "https://graph.microsoft.com/v1.0/me/events/delta?$deltatoken=ABC", // delta-resumption responses
313
- "count": 42 // when @odata.count is present
314
- }
71
+ **Login flow:** By default, the CLI uses Playwright to open a browser for authentication. Alternatively, you can use the [Ask Marcel Companion](./browser-extension/) browser extension for faster authentication.
315
72
 
316
- // Error
317
- { "ok": false, "error": "Authentication cancelled" }
73
+ ```bash
74
+ ask-marcel login # default: Playwright browser
75
+ ask-marcel login --use-extension # use browser extension (requires setup)
318
76
  ```
319
77
 
320
- `@odata.nextLink`, `@odata.deltaLink`, and `@odata.count` from the Graph payload are lifted to the envelope's top level and removed from `data`, so consumers don't have to know the OData spelling. **Always check the top-level `nextLink` (and `deltaLink` for `*-delta` commands) — never reach into `data["@odata.nextLink"]`; it's been moved.** This applies uniformly across every paginated `list-*` / `search-*` / `*-delta` command.
78
+ **Browser extension setup (optional, one-time):**
321
79
 
322
- ### OData query passthrough
80
+ 1. Open `chrome://extensions/` (Chrome) or `edge://extensions/` (Edge)
81
+ 2. Enable "Developer mode" (top-right toggle)
82
+ 3. Click "Load unpacked" → select the `browser-extension/` folder from this repo
83
+ 4. **Enable in incognito/inprivate mode:** Click the extension's "Details" button, then enable "Allow in incognito" (Chrome) or "Allow in InPrivate" (Edge). **This is required** — the CLI opens an incognito/inprivate window, and extensions are disabled there by default.
84
+ 5. Done — use `ask-marcel login --use-extension` to authenticate via the extension
323
85
 
324
- Most `list-*`, `search-*`, and `*-delta` commands accept the standard OData query parameters as optional flags. Use them to shrink large responses on the fly — particularly important for context-window-bound LLM consumers:
86
+ ### Stable error envelope with actionable hints
325
87
 
326
- ```bash
327
- ask-marcel list-mail-messages --top 5 --select id,subject,from,receivedDateTime
328
- ask-marcel list-recent-files --filter "name eq 'budget.xlsx'" --orderby lastModifiedDateTime desc
329
- ask-marcel list-folder-files --drive-id b!abc --item-id 01DEF --select id,name --top 10
330
- ```
88
+ 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.
331
89
 
332
- The canonical set is `--top <n>`, `--skip <n>`, `--select <csv>`, `--filter <kql>`, `--orderby <kql>`, `--expand <nav>`. `--top` is capped at 1000 with a clear validation error (Graph silently truncates beyond that on every endpoint). **The CLI advertises only the flags the underlying Graph endpoint honors — flags Graph silently rejects or ignores are dropped from the option set, so the manifest never lies.** Narrower variants:
90
+ ### Lean responses
333
91
 
334
- - **No `--skip`** on endpoints Graph rejects it: `list-folder-files`, `list-drive-item-permissions`, `list-drive-item-versions`, `list-drive-item-thumbnails`, `search-onedrive-files`, `search-my-documents`, `get-drive-delta`, `get-drive-root-delta`, `list-recent-files`, `list-followed-drive-items`, `list-sharepoint-site-drives`, `list-sharepoint-site-list-items`, `list-sharepoint-list-item-versions`, `list-site-content-types`, `list-sharepoint-site-pages`, `list-groups`, `list-sharepoint-site-lists`, `list-sharepoint-list-columns`, `list-site-columns` (paginate via `nextLink` `next-page` instead).
335
- - **`--filter` / `--orderby` dropped** on Excel listings (Graph silently ignores them): `list-excel-tables`, `list-excel-worksheets` (also no `--top`), `list-excel-table-rows`.
336
- - **`--select` only** on Planner listings (`list-planner-plans`, `list-plan-buckets`) and `list-team-installed-apps` (which hard-pins `$expand=teamsAppDefinition` server-side).
337
- - **`--top`, `--skip`, `--filter`, `--expand` only** on `list-todo-task-lists` (Graph rejects `$select` and `$orderby`).
338
- - **No OData at all** on `list-shared-with-me`, `list-mail-rules`, `list-outlook-categories`, `get-mailbox-settings` — Graph silently ignores every passthrough; slice client-side.
339
- - **`--top` only** on the delta endpoints `list-calendar-events-delta` and `list-calendar-view-delta` (translated internally to `Prefer: odata.maxpagesize`; `$top` as a query parameter is rejected by Graph).
340
- - **`--filter` + `--select` only** on `list-team-channels`; **`--filter` omitted** on `list-conversation-messages`, `list-incomplete-todo-tasks`, `list-incomplete-planner-tasks`, `search-onenote-pages` (their path pins one).
92
+ 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`.
341
93
 
342
- `list-todo-tasks` rewrites Graph's opaque `RequestBroker--ParseUri` to a clear hint when `--select` / `--orderby` trips the title-quirk; `list-calendar-event-instances` rewrites `ExpandSeries can only be performed against a series` to a pointer at `--filter "type eq 'seriesMaster'"`; `list-my-direct-reports` auto-injects the `ConsistencyLevel: eventual` header Graph requires for `--orderby` on directory endpoints. `get-excel-range` caps the in-flight range at 100 000 cells to prevent runaway responses.
94
+ ### Saves big binaries to disk so they never hit the model's context
343
95
 
344
- ### Writing bytes to disk (`--output-path`)
96
+ 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.
345
97
 
346
- Every download / convert command (PDF, image, raw bytes, MIME, OneNote HTML, the markdown converters) returns its bytes as `{ contentType, size, base64 }` (binary) or `{ contentType, size, text }` (text). In default text mode the binary variant prints `binary: <contentType>, <size> bytes — use --output-path to save` rather than spilling base64 to stdout. For multi-MB payloads — a 5 MB PDF round-tripped through stdout would blow most LLM context windows — pass the **global** `--output-path <path>` flag and the CLI lands the bytes locally:
98
+ Two flag patterns avoid the round-trip:
99
+
100
+ - **`--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.
101
+ - **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.
102
+
103
+ ### Relative dates on calendar windows
104
+
105
+ `--start-date-time "start-of-week" --end-date-time "+7d"`. No timestamp math before answering "what's on my calendar this week".
106
+
107
+ ## 30-second quickstart
347
108
 
348
109
  ```bash
110
+ # install (Bun ≥1.0 or Node ≥20)
111
+ npm i -g ask-marcel-office-cli
112
+
113
+ # authenticate (cached → refresh → browser fallback)
114
+ ask-marcel login
115
+
116
+ # the rest is read-only (the only writes are mail drafts) and discoverable from --help
117
+ ask-marcel list-drives
118
+ ask-marcel search-onedrive-files --drive-id "b!abc..." --query "Q3 budget"
119
+ ask-marcel convert-mail-to-markdown --message-id "AAMkAD..."
120
+ ask-marcel list-calendar-view --start-date-time today --end-date-time +7d
349
121
  ask-marcel convert-mail-attachment-to-pdf \
350
122
  --message-id "AAMkAD..." --attachment-id "AAMkAD...attach1" \
351
123
  --output-path /tmp/deck.pdf
352
- # Text mode:
353
- # contentType: application/pdf
354
- # size: 4837291
355
- # savedTo: /tmp/deck.pdf
356
- # JSON mode (--output json):
357
- # {"ok":true,"data":{"contentType":"application/pdf","size":4837291,"savedTo":"/tmp/deck.pdf"}}
358
124
  ```
359
125
 
360
- `--output-path` decodes `base64` (or writes `text`) to the path and replaces the inline field with `savedTo: <path>` in the response. Parent directories are created on demand. Applying the flag to a command that returns plain JSON (no `base64` / no `text` field — e.g. `get-current-user`) returns a clear `--output-path: <cmd> did not return inlined bytes …` error rather than silently writing nothing — a JSON-only command paired with this flag is almost certainly a mistake. The CLI follows any SharePoint media-transform redirect internally, so the LLM never has to fetch an external URL.
126
+ ## Asking the CLI what it can do
361
127
 
362
- `help-json` and `docs <cmd>` also honour `--output-path` (the manifest JSON and per-command Markdown are written to disk and the envelope reports `savedTo`). Paths ending in `/` or `\` are rejected upfront with "must be a file path, not a directory" instead of leaking Node's `EISDIR`. When a `*-as-pdf` command falls back to raw source bytes (`passthrough: true`), the CLI refuses to write a `.pdf` extension — pick the source extension instead, so a corrupt save is impossible.
128
+ Five discovery surfaces, each tuned for a different audience and token budget:
363
129
 
364
- ### Pagination
130
+ | When you want | Run | Returns |
131
+ |---|---|---|
132
+ | Help with a single command | `ask-marcel <command> --help` | Required flags, optional flags, an example, pagination notes |
133
+ | A scan of every command | `ask-marcel --help` | One-sentence summary per command, grouped by category |
134
+ | The slim LLM-friendly index | `ask-marcel 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 |
135
+ | The slim index for one domain | `ask-marcel 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 |
136
+ | Rich docs for one command | `ask-marcel docs <command>` | Full Markdown to stdout (response shape, examples, the underlying Graph endpoint, Microsoft Learn link) |
365
137
 
366
- When a response contains a `nextLink` cursor, feed that URL back through `next-page` and repeat until the cursor is gone. In text mode the cursor is the value after `next:` in the `---` footer line; in JSON mode it's the top-level `nextLink` field. The script below uses `--output json` because `jq` needs the JSON envelope:
138
+ 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.
367
139
 
368
- ```bash
369
- # page 1
370
- ask-marcel --output json list-mail-folders > p1.json
371
-
372
- # page 2..N — loop until nextLink is gone
373
- next=$(jq -r '.nextLink // empty' p1.json)
374
- while [ -n "$next" ]; do
375
- ask-marcel --output json next-page --url "$next" > pN.json
376
- next=$(jq -r '.nextLink // empty' pN.json)
377
- done
378
- ```
140
+ ## Use it from Claude Code, Cursor, Cline, or any tool-calling LLM
141
+
142
+ Most agents already know how to read JSON from stdout. Two patterns work:
379
143
 
380
- Every paginated command advertises this in three places: `ask-marcel <cmd> --help` prints a `Pagination:` line, `ask-marcel docs <cmd>` adds a `**Pagination:**` field, and [`docs/commands.json`](docs/commands.json) ships `"pagination": true` on each entry so agents can detect it programmatically.
144
+ **1. Drop in as a shell tool** the agent learns the manifest, then runs `ask-marcel <command> --output json`. The slim defaults + structured error hints mean it can self-recover from typos.
381
145
 
382
- ### Quick context
146
+ **2. Embed as a library** — every command is exported. Compose it inside your own MCP server, Claude Agent, or LangChain tool:
383
147
 
384
- `ask-marcel my-quick-context` returns `{ user, primaryDriveId, inboxId, todoLists, primaryCalendarId }` in a single round trip — five Graph calls in parallel. Use it as the first call in any LLM session that needs per-user IDs to feed into other commands.
148
+ ```ts
149
+ import { commands, buildDeps } from 'ask-marcel-office-cli';
385
150
 
386
- ## Usage (library)
151
+ const { graph } = buildDeps();
152
+ const result = await commands['list-mail-messages'].execute(graph, { top: '10' });
153
+ if (result.ok) {
154
+ // result.value is the Graph payload — typed Result<unknown, GraphError>
155
+ }
156
+ ```
387
157
 
388
- The package exports a typed library API for embedding inside your own CLI, agent, or service.
158
+ ### Auth two paths
159
+
160
+ **Most users — use the built-in browser-OAuth ladder:**
389
161
 
390
162
  ```ts
391
- import { commands, createGraphClient, buildDeps, type Result } from 'ask-marcel-office-cli';
163
+ import { buildDeps } from 'ask-marcel-office-cli';
392
164
 
393
- // option 1 — full ladder with built-in OAuth and file cache
394
165
  const { graph } = buildDeps();
395
- const result = await commands['list-drives'].execute(graph, {});
396
- if (result.ok) console.log(result.value);
166
+ // First call triggers cache → refresh → headed-Chromium fallback automatically.
167
+ // Tokens cached at ~/.ask-marcel/token-cache.json for subsequent calls.
168
+ ```
169
+
170
+ **Agents / CI / MCP servers — bring your own token:**
171
+
172
+ ```ts
173
+ import { createGraphClient } from 'ask-marcel-office-cli';
397
174
 
398
- // option 2 — bring your own AuthManager / token
399
175
  const graph = createGraphClient({
400
- getAccessToken: async () => ({ ok: true, value: process.env.MS_GRAPH_TOKEN as never }),
176
+ getAccessToken: async () => ({
177
+ ok: true,
178
+ value: await fetchTokenFromYourVault(),
179
+ }),
401
180
  logout: async () => ({ ok: true, value: undefined }),
402
181
  });
403
- const me = await commands['get-current-user'].execute(graph, {});
404
182
  ```
405
183
 
406
- The full export list (registry, factories, `Result`, branded types, ports) is in [src/index.ts](src/index.ts).
184
+ 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.
407
185
 
408
- ## Architecture
186
+ ## Deep docs
409
187
 
410
- ```
411
- src/
412
- domain/ Result<T,E>, branded value-object types (AccessToken, EnvVar), JWT utilities, format-error
413
- infra/ Auth recovery ladder (cache refresh Playwright browser), Graph API HTTP client, Winston logger
414
- use-cases/ — Commands (schemas + execute functions), ports
415
- composition/ — CLI wiring (Commander), dependency graph
416
- presenter/ — Output formatting (text YAML-ish default + JSON envelope opt-in)
417
- ```
418
-
419
- - **Auth**: Three-rung recovery ladder — file-based cached JWT → OAuth refresh_token exchange → Playwright browser intercepting Teams login
420
- - **Client ID**: `5e3ce6c0-2b1f-4285-8d4b-75ee78787346` (Teams Web)
421
- - **Scopes**: `https://graph.microsoft.com/.default openid profile offline_access`
422
- - **Token cache**: `~/.ask-marcel/token-cache.json` (overridable via `BuildDepsConfig.cachePath`)
423
- - **Browser profile**: `~/.ask-marcel/browser-profile` (overridable via `ASKMARCEL_BROWSER_PROFILE`)
424
- - **Output**: YAML-ish text by default (LLM-readable, ~30–60% fewer tokens than JSON on listings); compact JSON envelope via `--output json` for tool-chaining and `jq` pipelines
188
+ - **[All 177 commands](docs/COMMANDS.md)** — per-category tables with required params + Graph endpoint
189
+ - **[Usage guide](docs/USAGE.md)** — output formats, OData passthrough, `--output-path`, pagination, library API, architecture, configuration, quality gates
190
+ - **[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'`
191
+ - **[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
425
192
 
426
- ### Elevated token (historical-version downloads)
427
193
 
428
- Three commands need a Graph token whose `appid` is on Microsoft's ODSP allow-list — the Teams web client token returns 403 with `logicalPermissionAccessDenied` against historical-version bytes:
194
+ ## Agent skill (progressive disclosure)
429
195
 
430
- - `download-drive-item-version-content`
431
- - `download-drive-item-version-as-markdown`
432
- - `download-drive-item-version-as-pdf`
196
+ A [Codex skill](https://docs.anthropic.com/en/docs/agents-and-tools/codex) lives at `.agents/skills/ask-marcel-office/` and teaches agents how to use the CLI without loading all 177 commands into context at once.
433
197
 
434
- Login captures a *second* Graph token from `https://m365.cloud.microsoft/search` whose first-party identity is M365ChatClient (`c0ab8ce9-e9a0-42e7-b064-33d422df41f1`) — an app on the ODSP allow-list. It is stored alongside the Teams token (`elevated_access_token` / `elevated_expires_on` fields in the cache) and used only by the three commands above. Refresh path is re-capture via a brief Edge launch — the persistent profile cookies do silent SSO when fresh; if the federated IdP session has lapsed (e.g. Okta-fronted tenants), interactive sign-in completes inside the popup. If the elevated capture fails at login, the other 150+ commands (including `list-chats` / `get-chat`, which use the regular Teams token) still work.
198
+ **Structure**
435
199
 
436
- ## Configuration
200
+ ```
201
+ .agents/skills/ask-marcel-office/
202
+ ├── SKILL.md # core workflow + category index
203
+ └── references/ # per-domain command details, loaded on demand
204
+ ├── marcel-mail.md (32 commands)
205
+ ├── marcel-drive.md (30 commands)
206
+ ├── marcel-calendar.md (23 commands)
207
+ ├── marcel-sharepoint.md (18 commands)
208
+ ├── marcel-user.md (15 commands)
209
+ ├── marcel-tasks.md (15 commands)
210
+ ├── marcel-excel.md (11 commands)
211
+ ├── marcel-notes.md (11 commands)
212
+ ├── marcel-chats.md (9 commands)
213
+ ├── marcel-teams.md (7 commands)
214
+ └── marcel-meta.md (6 commands)
215
+ ```
437
216
 
438
- Environment variables read at composition time:
217
+ **How it works**
439
218
 
440
- | Variable | Used by | Default |
441
- |---|---|---|
442
- | `ASKMARCEL_LOG_LEVEL` | Winston logger; all log output goes to **stderr** (stdout reserved for command output — text by default, JSON under `--output json`). Namespaced so a generic `LOG_LEVEL` exported by another tool in your shell does not leak into ours. | `error` (use `info` or `debug` for troubleshooting) |
443
- | `HOME` / `USERPROFILE` | Default cache and browser-profile paths | _(required)_ |
444
- | `ASKMARCEL_BROWSER_PROFILE` | Override Playwright user-data-dir | _(none)_ |
219
+ `SKILL.md` contains the authentication flow, the discovery loop (`help-json --terse --category` → `docs <cmd>` → execute), key patterns (OData passthrough, relative dates, document conversion, pagination), and a category index. The full command reference for each domain lives in `references/marcel-<category>.md` and is loaded only when the agent needs that domain — keeping the context window lean.
445
220
 
446
- `HTTP_PROXY` / `HTTPS_PROXY` / `http_proxy` / `https_proxy` are stripped from the process environment immediately before launching Playwright (see `src/infra/browser-auth.ts`).
221
+ ## Roadmap
447
222
 
448
- ## Quality gates (atelier four-check loop)
223
+ Read-only stays the default forever. There's no fixed feature backlog — coverage grows out of real LLM workflows as they come up.
449
224
 
450
- ```bash
451
- bun test # full suite
452
- bun run lint # ESLint (0 warnings, 0 errors)
453
- bun run typecheck # tsc --noEmit
454
- bun run coverage # per-tier gates (100% on every tier: domain, use-cases, infra, composition, presenter)
455
- bun run mutate:changed # mutation testing on changed domain/use-case files (>90% kill threshold)
456
- ```
225
+ Suggestions, requests, and pull requests welcome — see the [issues page](https://github.com/vdelacou/ask-marcel-office-cli/issues).
457
226
 
458
- ### Pre-commit hook (atelier 8 gates)
227
+ ## Built with
459
228
 
460
- The repo ships an 8-gate hook at `.githooks/pre-commit` (commit size package.json gitleaks tests strict lint typecheck coverage mutation). Install once per clone:
229
+ - **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.
230
+ - **Microsoft Graph v1.0** — the public API surface, no beta endpoints in production code.
231
+ - **Playwright** — headed Chromium for the first-launch browser-OAuth dance.
461
232
 
462
- ```bash
463
- git config core.hooksPath .githooks
464
- ```
233
+ ## License
465
234
 
466
- Optional but recommended: install [gitleaks](https://github.com/gitleaks/gitleaks) (`brew install gitleaks`) to enable gate 3. The hook degrades gracefully if it's missing.
235
+ MIT © Vincent Delacourt