ask-marcel-office-cli 1.4.0 → 1.5.1

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 (107) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +68 -24
  3. package/dist/cli.js +7641 -90932
  4. package/dist/commands.json +1230 -70
  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 +1 -1
  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 +10431 -93852
  11. package/dist/infra/auth.d.ts +27 -3
  12. package/dist/infra/browser-auth.d.ts +25 -0
  13. package/dist/infra/graph-client.d.ts +3 -1
  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 +1 -1
  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 +2 -2
  28. package/dist/presenter/output.d.ts +1 -1
  29. package/dist/use-cases/commands/build-command.d.ts +1 -1
  30. package/dist/use-cases/commands/command-types.d.ts +42 -1
  31. package/dist/use-cases/commands/convert-calendar-event-attachment-to-markdown.d.ts +15 -0
  32. package/dist/use-cases/commands/convert-calendar-event-attachment-to-pdf.d.ts +11 -0
  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 +4 -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 +3 -0
  42. package/dist/use-cases/commands/docs.d.ts +1 -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 +4 -0
  49. package/dist/use-cases/commands/extract-drive-item-images.d.ts +11 -0
  50. package/dist/use-cases/commands/extract-local-file-images.d.ts +21 -0
  51. package/dist/use-cases/commands/extract-mail-attachment-images.d.ts +11 -0
  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 +1 -0
  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/first-sentence.d.ts +12 -0
  58. package/dist/use-cases/commands/get-excel-chart-image.d.ts +20 -0
  59. package/dist/use-cases/commands/get-onenote-page-as-markdown.d.ts +8 -0
  60. package/dist/use-cases/commands/get-todo-task.d.ts +9 -2
  61. package/dist/use-cases/commands/image-extraction.d.ts +10 -0
  62. package/dist/use-cases/commands/iso-datetime-schema.d.ts +1 -1
  63. package/dist/use-cases/commands/list-accessible-drives.d.ts +14 -0
  64. package/dist/use-cases/commands/list-calendar-event-attachments.d.ts +14 -0
  65. package/dist/use-cases/commands/mail-quote-stripper.d.ts +25 -0
  66. package/dist/use-cases/commands/markdown-dispatch.d.ts +29 -0
  67. package/dist/use-cases/commands/markdown-pipeline.d.ts +6 -2
  68. package/dist/use-cases/commands/media-files.d.ts +20 -0
  69. package/dist/use-cases/commands/msg-to-markdown.d.ts +28 -0
  70. package/dist/use-cases/commands/odf-content-to-markdown.d.ts +5 -0
  71. package/dist/use-cases/commands/odf-metadata-to-markdown.d.ts +3 -0
  72. package/dist/use-cases/commands/odf-metadata.d.ts +26 -0
  73. package/dist/use-cases/commands/odf-to-markdown.d.ts +17 -0
  74. package/dist/use-cases/commands/office-extensions.d.ts +13 -0
  75. package/dist/use-cases/commands/office-to-markdown.d.ts +6 -2
  76. package/dist/use-cases/commands/onenote-metadata.d.ts +21 -0
  77. package/dist/use-cases/commands/onenote-resource-embedder.d.ts +3 -0
  78. package/dist/use-cases/commands/ooxml-metadata-to-markdown.d.ts +16 -0
  79. package/dist/use-cases/commands/ooxml-metadata.d.ts +23 -0
  80. package/dist/use-cases/commands/ooxml-xml-walker.d.ts +36 -0
  81. package/dist/use-cases/commands/option-descriptions.d.ts +5 -0
  82. package/dist/use-cases/commands/output-path.d.ts +23 -0
  83. package/dist/use-cases/commands/pdf-to-markdown.d.ts +19 -0
  84. package/dist/use-cases/commands/pptx-comments.d.ts +24 -0
  85. package/dist/use-cases/commands/pptx-metadata-to-markdown.d.ts +3 -0
  86. package/dist/use-cases/commands/pptx-metadata.d.ts +33 -0
  87. package/dist/use-cases/commands/pptx-slides.d.ts +17 -0
  88. package/dist/use-cases/commands/pptx-to-markdown.d.ts +7 -0
  89. package/dist/use-cases/commands/read-mail-attachment.d.ts +15 -0
  90. package/dist/use-cases/commands/search-all-accessible-sites.d.ts +12 -0
  91. package/dist/use-cases/commands/search-index-total.d.ts +3 -0
  92. package/dist/use-cases/commands/search-sharepoint-sites-by-name.d.ts +3 -2
  93. package/dist/use-cases/commands/sharepoint-link-extractor.d.ts +17 -12
  94. package/dist/use-cases/commands/text-passthrough.d.ts +15 -2
  95. package/dist/use-cases/commands/todo-parse-uri-rewrite.d.ts +7 -0
  96. package/dist/use-cases/commands/update-mail-draft.d.ts +22 -0
  97. package/dist/use-cases/commands/xlsx-comments.d.ts +29 -0
  98. package/dist/use-cases/commands/xlsx-metadata-to-markdown.d.ts +3 -0
  99. package/dist/use-cases/commands/xlsx-metadata.d.ts +39 -0
  100. package/dist/use-cases/commands/xlsx-to-markdown.d.ts +9 -2
  101. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +30 -0
  102. package/dist/use-cases/ports/filesystem.d.ts +10 -0
  103. package/docs/COMMANDS.md +34 -18
  104. package/docs/USAGE.md +25 -8
  105. package/docs/commands.json +1230 -70
  106. package/package.json +14 -8
  107. /package/dist/use-cases/commands/{download-onedrive-file-content.d.ts → download-drive-item-content.d.ts} +0 -0
@@ -0,0 +1,30 @@
1
+ import type { Result } from '../../domain/result.js';
2
+ import type { GraphError } from '../../infra/graph-client.js';
3
+ /**
4
+ * Shared "unzip + convert every contained file" core behind
5
+ * `convert-drive-item-zip` (a OneDrive / SharePoint .zip),
6
+ * `convert-mail-attachment-zip` (an Outlook .zip attachment), and
7
+ * `convert-local-file` (a .zip on disk). Each entry is run through the same
8
+ * `bytesToMarkdown` dispatch the markdown commands use; an entry the dispatch
9
+ * can't convert (image, binary, nested archive, scanned PDF) is LISTED with a
10
+ * note instead of failing the whole archive. Notes use the container-neutral
11
+ * NESTED_HINTS (QA-007): entries live INSIDE the zip, so caller-specific
12
+ * sibling-command pointers (`extract-drive-item-images`, …) cannot reach them.
13
+ */
14
+ declare const MAX_ENTRIES = 100;
15
+ type FileResult = {
16
+ readonly path: string;
17
+ readonly contentType?: string;
18
+ readonly size?: number;
19
+ readonly text?: string;
20
+ readonly note?: string;
21
+ };
22
+ type ZipArchiveResult = {
23
+ readonly count: number;
24
+ readonly truncated?: true;
25
+ readonly totalEntries?: number;
26
+ readonly files: ReadonlyArray<FileResult>;
27
+ };
28
+ declare const convertZipArchive: (bytes: Uint8Array, includeMetadata: boolean) => Promise<Result<ZipArchiveResult, GraphError>>;
29
+ export { convertZipArchive, MAX_ENTRIES };
30
+ export type { FileResult, ZipArchiveResult };
@@ -10,8 +10,18 @@ export type FileSystemError = {
10
10
  };
11
11
  export type FileSystem = {
12
12
  readonly readJson: <T>(path: string) => Promise<Result<T, FileSystemError>>;
13
+ /**
14
+ * Read a file's raw bytes. Used by `convert-local-file` to feed a local
15
+ * document into the same conversion dispatch the Graph-backed commands use.
16
+ */
17
+ readonly readBytes: (path: string) => Promise<Result<Uint8Array, FileSystemError>>;
13
18
  readonly writeText: (path: string, content: string) => Promise<Result<void, FileSystemError>>;
14
19
  readonly writeBytes: (path: string, bytes: Uint8Array) => Promise<Result<void, FileSystemError>>;
20
+ /**
21
+ * Restrict a file's permission bits (e.g. 0o600 on the token cache so
22
+ * other local users cannot read cached secrets — QA-001).
23
+ */
24
+ readonly chmod: (path: string, mode: number) => Promise<Result<void, FileSystemError>>;
15
25
  readonly deleteIfExists: (path: string) => Promise<Result<void, FileSystemError>>;
16
26
  /**
17
27
  * Recursively delete a directory (and all its contents). Used by `logout`
package/docs/COMMANDS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Command reference
2
2
 
3
- All 165 commands across 12 categories, grouped by category. Each row shows the command name, a one-line summary, the required parameters, and the underlying Microsoft Graph endpoint.
3
+ All 176 commands across 12 categories, grouped by category. Each row shows the command name, a one-line summary, the required parameters, and the underlying Microsoft Graph endpoint.
4
4
 
5
5
  The auto-generated tables below are rebuilt from the live command registry on every `bun run docs:gen` / `bun run build` — they cannot drift from the actual surface.
6
6
 
@@ -28,10 +28,13 @@ For everything else:
28
28
 
29
29
  | Command | Description | Required params | Graph endpoint |
30
30
  |---------|-------------|-----------------|----------------|
31
- | `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` |
32
- | `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` |
33
- | `download-drive-item-version` | Download a *non-current* historical version of a OneDrive / SharePoint file. `--format original` (default) returns the raw bytes 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`. `--format pdf` runs Graph `?format=pdf` for Office docs; plain-text and `pdf` sources short-circuit to raw bytes with `passthrough: true` + a note (Graph rejects `pdf pdf` with InputFormatNotSupported). `--format markdown` runs the local conversion pipeline (mammoth for docx, sheetjs for xlsx, csv → table, plain-text passthrough). All three formats use an M365ChatClient-elevated Graph token (captured at login from m365.cloud.microsoft) — the Teams web client token returns 403 logicalPermissionAccessDenied on historical-version stream content. The CLI follows the SharePoint streamContent redirect internally so the LLM never has to fetch an external URL. Audit v1.0.0 §D4 caveat for `--format pdf`: Graph sometimes silently falls back to raw source bytes for the current version (which Graph occasionally serves through this endpoint) — when the response carries `passthrough: true`, save with the source extension, not `.pdf` (the global output-path flag refuses the mismatch). | `--drive-id`, `--item-id`, `--version-id`, `--format` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content` |
34
- | `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` |
31
+ | `convert-drive-item-zip` | Unzip a `.zip` from a OneDrive / SharePoint item and convert every contained file in one call so "read the handover archive" doesn't need a separate unzip + per-file conversion. Office files (docx/xlsx/pptx/odt/ods/odp and their macro-enabled / template variants) are converted to markdown via the local pipelines; plain-text entries (txt/md/csv/json/yaml/…) are decoded inline; legacy OLE .xls (sheetjs) and .doc (word-extractor, text only) are extracted; an Outlook .msg entry is rendered to markdown (headers + body, with its own attachments converted recursively); PDFs have their text layer extracted (text/plain); images, binaries, nested archives, legacy .ppt, and scanned/image-only PDFs (no text layer) are listed with a note (not unpacked) so one unsupported entry never fails the whole archive. Pass `--include-metadata true` to append each Office file's side-channel metadata block. Capped at 100 entries (the archive is buffered in memory); beyond that the response is flagged `truncated`. | `--drive-id`, `--item-id`, `--include-metadata` | `GET /drives/{drive-id}/items/{item-id}/content` |
32
+ | `download-drive-item-as-markdown` | Download a OneDrive / SharePoint file converted to markdown via local conversion pipelines. Supported: docx (mammoth → turndown; embedded images are replaced with `[image]` placeholders by default pass `--inline-images true` to embed them as base64 `data:` URIs, or pull the full-resolution originals with `extract-drive-item-images`; tables become GFM pipe tables), xlsx (one markdown table per sheet via sheetjs; any sheet whose used range exceeds the `--max-cells` cap, default 50 000, is summarised with a band-by-band read hint instead of a multi-hundred-MB table), csv (rendered as a markdown table), odt/ods/odp (OpenDocument body walked from content.xml — headings, lists, tables, named sheets, per-slide text, including style-hidden content), 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). pptx is flattened to per-slide text (titles, bullets, text-box / table text, with speaker notes inline) as `## Slide N` sections — document order, not guaranteed visual order; for layout / images / charts, `download-drive-item-as-pdf` + a vision-capable LLM reads the rendered deck better. pdf has its text layer extracted locally (via unpdf) and returned as `{ contentType: "text/plain", size, text }`; a scanned / image-only PDF with no text layer points you at `download-drive-item-as-pdf` + a vision model. Legacy OLE Office: .xls (Excel 97-2003) is read by sheetjs (same markdown-table path as .xlsx); .doc (Word 97-2003) is extracted by word-extractor as plain text (body only, no structure); .ppt (PowerPoint 97-2003) has no pure-JS path — use `download-drive-item-as-pdf` first, then a vision model. Outlook .msg (a saved email, also an OLE container) is rendered to markdown an H1 subject, a From/To/Cc/Date header block, the message body, then an `## Attachments` section where each attachment is converted recursively through this same pipeline (the same way a .zip is unpacked); inline / image attachments are listed with a note. For rtf/etc. use `download-drive-item-as-pdf` Graph `?format=pdf` accepts 38 input extensions. | `--drive-id`, `--item-id`, `--include-metadata`, `--inline-images`, `--max-cells` | `GET /drives/{drive-id}/items/{item-id}/content?format=html` |
33
+ | `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 back-to-back Graph round-trips; the `?format=pdf` transform can run up to the 5-minute request timeout on large or complex sources. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content?format=pdf` |
34
+ | `download-drive-item-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. The bytes are CONTENT-SNIFFED, not judged by extension: if they decode as valid UTF-8 they come back as `{contentType: "text/plain", size, text}` (avoids ~33% base64 bloat, works for any text file regardless of name); otherwise as `{contentType, size, base64}`. A binary file that happens to be named `.txt` is returned faithfully as base64 never silently corrupted into `�` by a forced text decode. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
35
+ | `download-drive-item-version` | Download a *non-current* historical version of a OneDrive / SharePoint file. `--format original` (default) returns the raw bytes — 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-drive-item-content`. `--format pdf` runs Graph `?format=pdf` for Office docs; plain-text and `pdf` sources short-circuit to raw bytes with `passthrough: true` + a note (Graph rejects `pdf → pdf` with InputFormatNotSupported). `--format markdown` runs the local conversion pipeline (mammoth for docx, sheetjs for xlsx, csv → table, odt/ods/odp via content.xml, plain-text passthrough). All three formats use an M365ChatClient-elevated Graph token (captured at login from m365.cloud.microsoft) — the Teams web client token returns 403 logicalPermissionAccessDenied on historical-version stream content. The CLI follows the SharePoint streamContent redirect internally so the LLM never has to fetch an external URL. Audit v1.0.0 §D4 caveat for `--format pdf`: Graph sometimes silently falls back to raw source bytes for the current version (which Graph occasionally serves through this endpoint) — when the response carries `passthrough: true`, save with the source extension, not `.pdf` (the global output-path flag refuses the mismatch). | `--drive-id`, `--item-id`, `--version-id`, `--format`, `--include-metadata` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content` |
36
+ | `extract-drive-item-images` | Extract the embedded images from a OneDrive / SharePoint document. For docx / xlsx / pptx (and their macro-enabled / template variants) it reads the OOXML media parts directly (png/jpg/gif/bmp/tiff/webp/svg) — including original full-resolution / un-cropped originals and images on hidden slides the rendered view never shows. For a pdf it walks every page via unpdf and re-encodes each painted image as PNG (note: page-oriented — it captures images as painted on each page, but NOT layer-hidden/unpainted XObjects or the full uncropped original behind a clipped image). Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded in the response. svg rides back as its XML source (which carries the diagram text labels); legacy vector (emf/wmf) and audio/video are skipped. For any other format the command returns a 415 pointing at `download-drive-item-content`. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
37
+ | `extract-sharepoint-links-in-documents` | Find every `*.sharepoint.com` URL embedded in a Word / Excel / PowerPoint or OpenDocument file on OneDrive or SharePoint 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. The document sibling of `extract-sharepoint-links-in-mail`. For OOXML (.docx/.xlsx/.pptx) it reads external hyperlinks from the package’s relationship parts (`_rels/*.rels`, `TargetMode="External"`); for OpenDocument (.odt/.ods/.odp) it reads the inline `xlink:href` links in content.xml / styles.xml — either way it catches links wherever they live (body text, headers/footers, cell formulas, slide shapes). Read-only — no conversion happens here. Capped at 25 unique URLs per call (returns `truncated: true` and `skippedCount` when there are more); duplicates are deduplicated; per-link errors are captured inside each entry instead of failing the whole call. Non-zip inputs (pdf/images) return an api_error. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
35
38
  | `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()` |
36
39
  | `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}` |
37
40
  | `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` |
@@ -39,7 +42,8 @@ For everything else:
39
42
  | `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` |
40
43
  | `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()` |
41
44
  | `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` |
42
- | `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}` |
45
+ | `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-drive-item-content`. | `--folder-name`, `--select`, `--expand` | `GET /me/drive/special/{folder-name}` |
46
+ | `list-accessible-drives` | Enumerate every drive (document library) the signed-in user can reach — personal OneDrive(s), Teams libraries, SharePoint M365-group sites, drives behind files shared with the user, private/shared Teams channel sites, drives behind recently-used / followed / trending items (activity signals), AND every NON-default document library of each discovered SharePoint site — by unioning `/me/drives`, `/me/joinedTeams`, `/me/memberOf` (Unified groups → `/groups/{id}/drive`), `/me/drive/sharedWithMe`, per-team `/teams/{id}/channels` → `/channels/{ch}/filesFolder` (private/shared channels only — their files live in their own site, not the team default drive), `/me/drive/recent` + `/me/drive/following` + `/me/insights/{trending,used,shared}`, and a path-addressed `/sites/{host}:/sites/{name}:/drives` per discovered site (catches secondary libraries like "Teams Wiki Data" the default-drive vectors miss). Unlike `search-sharepoint-sites-by-name` (which relies on the tenant search index and misses direct-link-only sites + OneDrives), these vectors surface drives the search index never returns; the index in turn returns sites you can open but are not a member of, so the *union of both commands* is the practical maximum on a delegated token. Each drive is tagged with the `sources[]` that found it (a drive can have several). Per-resource "can't reach this one" failures are dropped silently (404 no drive, 403 access-denied / non-member private channel, 423 admin-locked site, 400 stale/unresolvable id); only actionable failures (auth, throttling, 5xx, network) appear in `partialErrors[]`, so it stays signal-only. Fans out one `/groups/{id}/drive` + one `/teams/{id}/channels` call per joined team + member group, a `filesFolder` call per private/shared channel, five fixed activity calls, and one `/sites/{id}/drives` call per discovered site (all capped by `--max-groups`, default 100; raise carefully — large memberships can hit 429 throttling). `/me/followedSites` is not used — it 403s on this token. | `--max-groups`, `--count-files` | `GET /me/drives + /me/joinedTeams + /me/memberOf + /me/drive/sharedWithMe + per-group /groups/<id>/drive + per-team /teams/<id>/channels/<ch>/filesFolder + /me/drive/recent + /me/drive/following + /me/insights/<trending|used|shared> + per-site /sites/<host>:/sites/<name>:/drives` |
43
47
  | `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` |
44
48
  | `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` |
45
49
  | `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 the `download-drive-item-version` command (it accepts an `original | pdf | markdown` format selector); numeric coercion silently fails because Graph rejects `79` against a path templated for stringified floats. | `--drive-id`, `--item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/versions` |
@@ -47,11 +51,11 @@ For everything else:
47
51
  | `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` |
48
52
  | `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` |
49
53
  | `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` |
50
- | `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` |
54
+ | `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). Each item carries a `lastUsed` (a `usageDetails` object) with `lastAccessedDateTime` + `lastModifiedDateTime`. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/insights/used` |
51
55
  | `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` |
52
- | `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` |
56
+ | `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-drive-item-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` |
53
57
  | `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` |
54
- | `resolve-drive-share-link` | Encode a OneDrive / SharePoint sharing URL into the Graph `/shares/{token}` share token (`u!<base64url>` per [shares-get](https://learn.microsoft.com/en-us/graph/api/shares-get)). Pure transformation — no Graph call. Pipe the returned `graphPath` (`/shares/{token}/driveItem`) into a sibling lookup (`get-drive-item`, `download-onedrive-file-content`, `convert-mail-attachment-to-pdf`, etc.) once the file has been resolved to a `driveItem`. Accepts any `*.sharepoint.com` URL (tenant + `*-my.sharepoint.com` personal OneDrive) and Microsoft's short-link host `1drv.ms`. | `--url` | `GET {url}` |
58
+ | `resolve-drive-share-link` | Encode a OneDrive / SharePoint sharing URL into the Graph `/shares/{token}` share token (`u!<base64url>` per [shares-get](https://learn.microsoft.com/en-us/graph/api/shares-get)). Pure transformation — no Graph call. Pipe the returned `graphPath` (`/shares/{token}/driveItem`) into a sibling lookup (`get-drive-item`, `download-drive-item-content`, `convert-mail-attachment-to-pdf`, etc.) once the file has been resolved to a `driveItem`. Accepts any `*.sharepoint.com` URL (tenant + `*-my.sharepoint.com` personal OneDrive) and Microsoft's short-link host `1drv.ms`. | `--url` | `GET {url}` |
55
59
  | `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}')` |
56
60
  | `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}')` |
57
61
 
@@ -59,6 +63,7 @@ For everything else:
59
63
 
60
64
  | Command | Description | Required params | Graph endpoint |
61
65
  |---------|-------------|-----------------|----------------|
66
+ | `get-excel-chart-image` | Render a chart on an Excel worksheet as a PNG (base64). Calls Graph's chart `Image()` function (natural size, aspect-preserving) so a vision-capable LLM can read the plotted data itself — not just the chart's title / position metadata that `list-excel-worksheet-charts` returns. The chart id or name comes from `list-excel-worksheet-charts`. | `--drive-id`, `--item-id`, `--worksheet-id`, `--chart-id` | `GET /drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/charts/{chart-id}/Image(width=0,height=0,fittingMode='Fit')` |
62
67
  | `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}')` |
63
68
  | `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}` |
64
69
  | `get-excel-used-range` | Return the worksheet's used range — the bounding box of every non-empty cell — as a single Excel range. The CLI ships a slim default that strips the redundant `text` / `numberFormat` / `formulas` 2D arrays Graph returns (mostly `"General"` repeated cell-by-cell), keeping `address` / `rowCount` / `columnCount` / `values`. Pass `--full true` to return the raw four-array Graph shape. `--max-cells` (default 50 000) caps the size of the projected `values[]`; oversize ranges drop `values` and surface a hint pointing at `get-excel-range` for band-by-band reads. Avoids fetching the entire 1M × 16K-cell sheet when only a small data island is populated. | `--drive-id`, `--item-id`, `--worksheet-id`, `--full`, `--max-cells` | `GET /drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/usedRange()` |
@@ -90,6 +95,7 @@ For everything else:
90
95
  | `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` |
91
96
  | `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` |
92
97
  | `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` |
98
+ | `search-all-accessible-sites` | Enumerate EVERY SharePoint site the signed-in user can access via the Microsoft Search index — far more than `search-sharepoint-sites-by-name`, which calls `GET /sites?search=` and returns a single capped page with no continuation. This command deep-pages the Search API (`POST /search/query` with `entityTypes: ["site"]`) using `from`/`size`, following the index's own `moreResultsAvailable` flag until exhausted (or the page ceiling of 60×25 = 1500 is reached, signalled by `truncated: true`), and dedupes site resources by id. The index is security-trimmed, so it returns sites you can open even when you are not a member (the gap `list-accessible-drives` cannot fill). Conversely it does NOT return OneDrives, private channel sites, or direct-link-only sites — so the *union of this command and `list-accessible-drives` is the practical maximum reachable on a delegated token* (a truly exhaustive list of every site in the tenant needs admin-only app permissions: `GET /sites/getAllSites`). Sites you cannot open are EXCLUDED so the list is not polluted with 404s: `nonNavigableExcluded` drops add-in app domains, `/contentstorage/` (SharePoint Embedded) containers, and `/_layouts/` system URLs by URL shape (no probe); each remaining site is probed (`GET /sites/{id}?$select=…,siteCollection`) and `archivedExcluded` drops archived / `423 resourceLocked` sites (e.g. an auto-archived OneDrive of a departed user) while `notFoundExcluded` drops probes that 404. Active personal OneDrives are kept. All three counters are omitted when 0. Optional `--query` narrows the index (default `*` = all accessible sites) and keeps the per-site probe cheap. | `--query`, `--count-files` | `POST /search/query` |
93
99
  | `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}` |
94
100
 
95
101
  ### Tasks (To Do + Planner)
@@ -100,7 +106,7 @@ For everything else:
100
106
  | `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}` |
101
107
  | `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}` |
102
108
  | `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` |
103
- | `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}` |
109
+ | `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,status`) or `--expand checklistItems` / `--expand linkedResources` to inline child collections. Known Graph quirk: any `--select` combo that includes `title` trips `RequestBroker--ParseUri` on this endpoint; the CLI rewrites that opaque error to a hint. | `--todo-task-list-id`, `--todo-task-id`, `--select`, `--expand` | `GET /me/todo/lists/{todo-task-list-id}/tasks/{todo-task-id}` |
104
110
  | `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` |
105
111
  | `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'` |
106
112
  | `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` |
@@ -116,11 +122,14 @@ For everything else:
116
122
 
117
123
  | Command | Description | Required params | Graph endpoint |
118
124
  |---------|-------------|-----------------|----------------|
119
- | `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}` |
125
+ | `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, odt/ods/odp via content.xml, pptx as per-slide text (titles + bullets + speaker notes inline), pdf via text-layer extraction (unpdf → text/plain), legacy .xls via sheetjs and legacy .doc via word-extractor (text only), an Outlook .msg attachment rendered to markdown — headers + body with its own attachments converted recursively — 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 layout / images, `convert-mail-attachment-to-pdf` + a vision model reads the rendered deck better. A scanned / image-only PDF (no text layer), legacy .ppt, and rtf/etc. point to the PDF sibling. Loop/Fluid/Whiteboard reference-attachments use Graph `?format=html` (the four inputs Microsoft documents). | `--message-id`, `--attachment-id`, `--include-metadata` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
120
126
  | `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}` |
121
- | `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. By default, inline images (`isInline:true` + `image/*` content-type, size 2 MB) are embedded as base64 `data:` URIs so the output is self-contained (non-image inline attachments are NOT embedded; oversize inline images are replaced with a placeholder note). For LLM callers that only want the text body, pass `--inline-images false` to skip the per-image bytes fetch entirely the body keeps raw `cid:<contentId>` references and the inline images surface in the file-attachments list so you can decide whether to fetch them separately via `get-mail-attachment`. File attachments are always 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`, `--inline-images` | `GET /me/messages/{message-id}` |
127
+ | `convert-mail-attachment-zip` | Unzip a `.zip` Outlook mail attachment and convert every contained file in one call the mail-side mirror of `convert-drive-item-zip`, so reading a zipped vendor deck doesn't need `get-mail-attachment` + manual `unzip` + per-file conversion. Pulls the fileAttachment bytes, unzips them (legacy GBK / CP437 entry names Chinese vendor archives written by WinRAR / Windows Explorer are decoded correctly, not mojibaked), and runs each file through the local pipelines: Office files (docx/xlsx/pptx/odt/ods/odp and macro-enabled / template variants) markdown; plain-text entries decoded inline; legacy OLE .xls (sheetjs) and .doc (word-extractor, text only) extracted; an inner Outlook .msg rendered; PDFs have their text layer extracted; images, binaries, nested archives, legacy .ppt, and scanned/image-only PDFs are listed with a note (not unpacked) so one unsupported entry never fails the whole archive. Pass `--include-metadata true` to append each Office file's side-channel metadata block. Capped at 100 entries; beyond that the response is flagged `truncated`. itemAttachment / referenceAttachment are rejected (no inline zip payload). | `--message-id`, `--attachment-id`, `--include-metadata` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
128
+ | `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. By default, inline images (`isInline:true` + `image/*` content-type, size ≤ 2 MB) are embedded as base64 `data:` URIs so the output is self-contained (non-image inline attachments are NOT embedded; oversize inline images are replaced with a placeholder note). For LLM callers that only want the text body, pass `--inline-images false` to skip the per-image bytes fetch entirely — the body keeps raw `cid:<contentId>` references and the inline images surface in the file-attachments list so you can decide whether to fetch them separately via `get-mail-attachment`. File attachments are always 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`, `--inline-images`, `--keep-quoted` | `GET /me/messages/{message-id}` |
129
+ | `create-mail-draft` | Create a new mail draft. POST /me/messages (or /me/mailFolders/{id}/messages when --mail-folder-id is set). The draft is saved in the Drafts folder (or the specified folder) and can be sent later via the Outlook client or Graph sendMail. Recipients are comma-separated email addresses. Returns the created message object with its id — use this id with update-mail-draft to modify the draft before sending. | `--subject`, `--body-content`, `--body-content-type`, `--to-recipients`, `--cc-recipients`, `--bcc-recipients`, `--importance`, `--mail-folder-id` | `POST /me/messages (or /me/mailFolders/{mail-folder-id}/messages)` |
130
+ | `extract-mail-attachment-images` | Extract the embedded images from an Outlook mail attachment that is a pdf or a docx / xlsx / pptx (and their macro-enabled / template variants). OOXML reads the media parts directly (png/jpg/gif/bmp/tiff/webp/svg), including full-resolution / un-cropped originals and images on hidden slides; pdf walks every page via unpdf and re-encodes each painted image as PNG (page-oriented — not layer-hidden/unpainted/uncropped originals). fileAttachment decodes the inline bytes; referenceAttachment resolves via /shares/{token}/driveItem and fetches the content. Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded. svg rides back as its XML source (which carries the diagram text labels); legacy vector (emf/wmf) and audio/video are skipped. itemAttachment and unsupported formats return a 415. | `--message-id`, `--attachment-id` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
122
131
  | `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}` |
123
- | `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}` |
132
+ | `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 — and when an output-path is set the CLI strips BOTH `contentBytes` and `base64` from stdout, leaving a compact metadata envelope with `savedTo` (the file is on disk; no multi-MB base64 in the terminal). When you only want metadata, use `--select id,name,contentType,size` to skip the `contentBytes` payload. | `--message-id`, `--attachment-id`, `--select`, `--expand` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
124
133
  | `get-mail-message` | Get a single Outlook message by ID. The CLI ships a slim default `--select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,isRead,importance,bodyPreview` so an LLM caller doesn't pull a 41 KB resource just to read a subject line. Pass `--select id,subject,body` (or any other comma-separated field list) to override; for the raw RFC-822 source use `get-mail-message-mime` instead. | `--message-id`, `--select`, `--expand` | `GET /me/messages/{message-id}` |
125
134
  | `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` |
126
135
  | `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}` |
@@ -141,14 +150,16 @@ For everything else:
141
150
  | `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` |
142
151
  | `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` |
143
152
  | `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` |
153
+ | `read-mail-attachment` | Read an Outlook mail attachment whatever it is — one command that auto-routes by file type, preferring the content-type when the filename extension is misleading (a real `.jpg` that is actually a spreadsheet still converts), so a caller never has to choose between the convert-mail-attachment-* siblings. A `.zip` fileAttachment is unpacked and every entry converted (mirrors `convert-mail-attachment-zip`, returning the `{ count, files }` envelope; legacy GBK/CP437 names decoded). Any other attachment — docx/xlsx/pptx/odt/ods/odp + macro/template variants → markdown, csv → table, pdf → text layer (with `pageCount`), legacy .xls/.doc extracted, an inner Outlook .msg rendered recursively, plain text passed through, referenceAttachment resolved via `/shares`, and itemAttachment (embedded mail/event/contact) rendered — goes through the same dispatch as `convert-mail-attachment-to-markdown` (returning its `{ contentType, size, text }` envelope). Images, scanned/image-only PDFs, and legacy .ppt return an actionable 415 pointing at `convert-mail-attachment-to-pdf` + a vision model or `get-mail-attachment` for the raw bytes. Pass `--include-metadata true` to append Office side-channel metadata. Use the explicit `convert-mail-attachment-to-markdown` / `-to-pdf` / `-zip` siblings only when you need to force a specific output format. | `--message-id`, `--attachment-id`, `--include-metadata` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
144
154
  | `resolve-mail-link` | Parse a Microsoft Outlook web mail link (the URL emitted by the "Copy link" / address-bar share of an email) into its `messageId`. Pure transformation — no Graph call. Pipe the result into `get-mail-message` to fetch the body, or `convert-mail-to-markdown` to render it. For Outlook calendar links use `resolve-calendar-link` instead — this command rejects them with a pointer. | `--url` | `GET {url}` |
145
155
  | `search-mail-messages` | Search the signed-in user's entire Outlook mailbox using KQL or free text. Results are ranked by Graph relevance. The CLI ships a slim default `--select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,isRead,importance,bodyPreview` (same as `list-mail-messages`) so a 3-result page stays ~3 KB instead of ~30 KB. Pass `--select id,subject,body` to widen, or override entirely. 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. **KQL quoting gotcha**: pass the raw KQL expression, e.g. `--query 'subject:invoice from:alice'`; do NOT wrap your terms in extra double-quotes (Graph then rejects with `BadRequest: An identifier was expected at position 0` because it sees `"..."` after the `$search=` interpolation). The CLI already wraps the entire `--query` value in `"..."` on the wire. | `--query`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages?$search="{query}"` |
156
+ | `update-mail-draft` | Update an existing mail draft. PATCH /me/messages/{id} — modifies a draft created by create-mail-draft (or any existing draft in the Drafts folder). Only the fields you pass are updated; omitted fields are left unchanged. At least one field must be provided. Returns the updated message object. Use get-mail-message to verify the final state before sending. | `--message-id`, `--subject`, `--body-content`, `--body-content-type`, `--to-recipients`, `--cc-recipients`, `--bcc-recipients`, `--importance` | `PATCH /me/messages/{message-id}` |
146
157
 
147
158
  ### Notes (OneNote)
148
159
 
149
160
  | Command | Description | Required params | Graph endpoint |
150
161
  |---------|-------------|-----------------|----------------|
151
- | `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` |
162
+ | `get-onenote-page-as-markdown` | Get the body of a single OneNote page as markdown. Graph returns OneNote pages as HTML, which this command runs through turndown locally. By default the page’s inline images (its `…/onenote/resources/{id}/$value` references) are fetched and embedded as base64 `data:` URIs so the markdown is self-contained — pass `--inline-images false` to keep the raw Graph resource URLs instead. Image embedding is per-image isolated: any resource that fails to fetch, is oversize (> 2 MB), or is not an image is left as a URL rather than failing the page. Pass `--include-metadata true` to append a `## OneNote metadata` block (title, created / last-modified timestamps, parent section + notebook). For the raw HTML use `get-onenote-page-content`. | `--onenote-page-id`, `--inline-images`, `--include-metadata` | `GET /me/onenote/pages/{onenote-page-id}/content` |
152
163
  | `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` |
153
164
  | `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` |
154
165
  | `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` |
@@ -184,9 +195,12 @@ For everything else:
184
195
 
185
196
  | Command | Description | Required params | Graph endpoint |
186
197
  |---------|-------------|-----------------|----------------|
198
+ | `convert-calendar-event-attachment-to-markdown` | Convert an attachment on an Outlook calendar event to markdown. Polymorphic on the attachment’s `@odata.type` (shares the mail-attachment pipeline): fileAttachment decodes the inline bytes and runs them through the local conversion pipeline (docx via mammoth, xlsx via sheetjs, csv as markdown table, odt/ods/odp via content.xml, plus plain-text passthrough); referenceAttachment resolves via /shares/{token}/driveItem and routes through the same dispatcher; itemAttachment (embedded mail / event / contact) is rendered locally. For pptx decks attached to a meeting, `convert-calendar-event-attachment-to-pdf` preserves slide layout (a pptx here yields only its speaker notes / titles / comments via `## PPTX metadata` with `--include-metadata true`). For pdf/rtf/etc. also use the PDF sibling. | `--event-id`, `--attachment-id`, `--include-metadata` | `GET /me/events/{event-id}/attachments/{attachment-id}` |
199
+ | `convert-calendar-event-attachment-to-pdf` | Convert an attachment on an Outlook calendar event to PDF on the fly (shares the mail-attachment pipeline). fileAttachment uploads the bytes to a temp folder under /me/drive, runs Graph `?format=pdf`, then deletes the temp item; referenceAttachment resolves via /shares/{token}/driveItem and converts in place; plain-text and `pdf` sources short-circuit to a raw-bytes envelope (Graph’s `?format=pdf` does not accept `pdf` as an input). image attachments are rejected (Graph rejects image inputs); itemAttachment (embedded mail/event/contact) is unsupported — use convert-calendar-event-attachment-to-markdown. Best for the deck attached to a meeting, where PDF preserves slide layout for a vision-capable LLM. | `--event-id`, `--attachment-id` | `GET /me/events/{event-id}/attachments/{attachment-id}` |
187
200
  | `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}` |
188
201
  | `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` |
189
202
  | `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}` |
203
+ | `list-calendar-event-attachments` | List the attachments (file, item, reference) on a single Outlook calendar event. Ships an opinionated default `--select=id,name,contentType,size,isInline` so an LLM doesn't accidentally pull multi-MB `contentBytes` for every attachment. The `@odata.type` discriminator is always returned by Graph regardless of `$select` (and Graph rejects asking for it explicitly). To read one, call `convert-calendar-event-attachment-to-markdown` (or `convert-calendar-event-attachment-to-pdf` for slide / layout fidelity). | `--event-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/events/{event-id}/attachments` |
190
204
  | `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}` |
191
205
  | `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` |
192
206
  | `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()` |
@@ -209,7 +223,7 @@ For everything else:
209
223
 
210
224
  | Command | Description | Required params | Graph endpoint |
211
225
  |---------|-------------|-----------------|----------------|
212
- | `find-chats-with-user` | Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Herve` matches `Hervé ATTANE` AND `herve.attane@lvmh.com` AND `HERVÉ` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. | `--name`, `--max-pages`, `--page-size` | `GET https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats` |
226
+ | `find-chats-with-user` | Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. | `--name`, `--max-pages`, `--page-size` | `GET https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats` |
213
227
  | `get-chat` | Return metadata for a single Microsoft Teams chat (1:1, group, or meeting). The CLI ships a slim default `--select=id,topic,chatType,createdDateTime,lastUpdatedDateTime`; pass `--select id,topic,webUrl,onlineMeetingInfo` (or any other comma-separated field list) to widen. Pass `--expand members` to inline membership. Returns metadata only — 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`, `--select`, `--expand` | `GET /chats/{chat-id}` |
214
228
  | `get-teams-chat-message` | Return a single Microsoft Teams chat message by its id via the chat substrate. Uses the chatsvcagg-audience bearer captured at login (same identity as the basic Teams token, different audience). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. Source the chat-id + message-id via `list-teams-chats-with-messages` or `list-teams-chat-messages`. | `--chat-id`, `--message-id` | `GET https://teams.microsoft.com/api/csa/{region}/api/v1/chats/{chat-id}/messages/{message-id}` |
215
229
  | `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` |
@@ -223,7 +237,7 @@ For everything else:
223
237
 
224
238
  | Command | Description | Required params | Graph endpoint |
225
239
  |---------|-------------|-----------------|----------------|
226
- | `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` |
240
+ | `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-drive-item-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` |
227
241
  | `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}` |
228
242
  | `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}` |
229
243
  | `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` |
@@ -235,9 +249,11 @@ For everything else:
235
249
 
236
250
  | Command | Description | Required params | Graph endpoint |
237
251
  |---------|-------------|-----------------|----------------|
252
+ | `convert-local-file` | Convert a file ON DISK to markdown — the only command that never calls Microsoft Graph (works offline, no login). Runs the same local pipelines as `download-drive-item-as-markdown`: docx (mammoth → turndown), xlsx (sheetjs tables, `--max-cells` OOM cap), pptx (per-slide text), odt/ods/odp, csv, pdf (text layer via unpdf), legacy OLE .xls / .doc, Outlook .msg (headers + body, attachments converted recursively), plain-text passthrough — and a `.zip` is unpacked with every contained file converted in one call (legacy GBK / CP437 entry names decoded, not mojibaked). What it canNOT do locally: convert TO pdf, and Loop/Fluid/Whiteboard sources — both need a Graph server round-trip (upload to OneDrive and use the drive-item siblings). Pass `--include-metadata true` for the Office side-channel metadata blocks; `--inline-images true` to embed docx images as base64 data URIs. | `--path`, `--include-metadata`, `--inline-images`, `--max-cells` | `GET (local) reads {path} from the local filesystem; not a Graph endpoint` |
253
+ | `extract-local-file-images` | Extract the embedded images from a file ON DISK — the local sibling of `extract-drive-item-images`, and like `convert-local-file` it never calls Microsoft Graph (works offline, no login). Same per-extension dispatch: docx / xlsx / pptx (and their macro-enabled / template variants) have their OOXML media parts read directly (png/jpg/gif/bmp/tiff/webp/svg — full-resolution originals, including images on hidden slides); a pdf is walked page by page via unpdf with each painted image re-encoded as PNG. Two flows only this command completes: a Graph-rendered PDF saved locally (legacy `.ppt` → `download-drive-item-as-pdf` with the global output-path flag → this command pulls the slide images for OCR), and Office files unpacked from a local archive. Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded. Any other extension returns a 415 naming the local ways out. | `--path` | `GET (local) reads {path} from the local filesystem; not a Graph endpoint` |
238
254
  | `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` |
239
- | `my-quick-context` | One-shot discovery for the IDs every other command needs, plus the user's job title and tenant timezone / locale / working-hours. Issues 9 Graph calls in parallel and returns what 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-planner-tasks`, `list-onenote-notebook-sections`, etc. For Microsoft To Do lists call `list-todo-task-lists` on demand (intentionally dropped from this command's fan-out — the array of {id, displayName, wellknownListName} entries crowded the envelope with IDs an LLM rarely needs on first contact). Audit Hervé-session §5.2: `tenantTimeZone` lets an LLM stop treating every datetime as UTC on first contact. | _(none)_ | `GET (meta) parallel: /me, /me/drive, /me/mailFolders/inbox, /me/calendar, /me/planner/plans, /me/onenote/notebooks, /me/joinedTeams, /me/drive/recent, /me/mailboxSettings` |
255
+ | `my-quick-context` | One-shot discovery for the IDs every other command needs, plus the user's job title and tenant timezone / locale / working-hours. Issues 9 Graph calls in parallel and returns what 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-planner-tasks`, `list-onenote-notebook-sections`, etc. For Microsoft To Do lists call `list-todo-task-lists` on demand (intentionally dropped from this command's fan-out — the array of {id, displayName, wellknownListName} entries crowded the envelope with IDs an LLM rarely needs on first contact). Audit Jane-session §5.2: `tenantTimeZone` lets an LLM stop treating every datetime as UTC on first contact. | _(none)_ | `GET (meta) parallel: /me, /me/drive, /me/mailFolders/inbox, /me/calendar, /me/planner/plans, /me/onenote/notebooks, /me/joinedTeams, /me/drive/recent, /me/mailboxSettings` |
240
256
  | `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}` |
241
- | `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). The `expiresInSeconds` field (added Hervé-session §4) lets an LLM decide pre-emptively to `login` again — typically worth doing under ~5 minutes (300 s) so a long-running session doesn't hit the wall mid-command. | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
257
+ | `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). The `expiresInSeconds` field (added Jane-session §4) lets an LLM decide pre-emptively to `login` again — typically worth doing under ~5 minutes (300 s) so a long-running session doesn't hit the wall mid-command. | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
242
258
 
243
259
  <!-- AUTO-GENERATED-COMMANDS:END -->
package/docs/USAGE.md CHANGED
@@ -18,8 +18,8 @@ bun add -g ask-marcel-office-cli
18
18
  ask-marcel login
19
19
 
20
20
  # the rest is discoverable
21
- ask-marcel --help # ~28 KB, one-sentence summaries
22
- ask-marcel help-json --terse --category mail # ~12 KB JSON for one category
21
+ ask-marcel --help # ~34 KB, one-sentence summaries
22
+ ask-marcel help-json --terse --category mail # ~6 KB JSON for one category
23
23
  ask-marcel docs list-mail-messages # full per-command Markdown
24
24
  ```
25
25
 
@@ -29,11 +29,11 @@ The first launch prints a one-time notice if a newer version is on npm.
29
29
 
30
30
  ## Output formats — `--output text` (default) vs `--output json`
31
31
 
32
- Every command writes its output as a single document to **stdout** (success or error — there 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.
32
+ Every command writes its output as a single document to **stdout** (success or error). stderr carries only diagnostics log lines when `ASKMARCEL_LOG_LEVEL` is raised, and `login`'s sign-in progress ("Browser window open — complete the sign-in…") so a long interactive capture is distinguishable from a hang — never command output. `process.exitCode` is `0` on success and `1` on any failure. Pick the format with the global `--output <text|json>` flag.
33
33
 
34
34
  ### Text (default, LLM-readable)
35
35
 
36
- YAML-ish `key: value` lines, generally smaller than the JSON envelope on long listings (the win grows with page size and shrinks toward parity on small projected pages — a 3-message page is ~3.3 KB in either format). Errors render as `error: <message>` followed by `hint:` and `source:` lines so an LLM can match the line shape without parsing JSON. Designed for LLMs reading and summarising; not for piping into other tools.
36
+ YAML-ish `key: value` lines, generally smaller than the JSON envelope on long listings (the win grows with page size and shrinks toward parity on small projected pages — a 3-message page is ~3.3 KB in either format). Errors render as `error: <message>` followed by `hint:`, `source:`, and (when Graph throttles) `retryAfter: Ns` lines so an LLM can match the line shape without parsing JSON. Designed for LLMs reading and summarising; not for piping into other tools.
37
37
 
38
38
  ```bash
39
39
  $ ask-marcel get-current-user
@@ -82,11 +82,20 @@ The stable `{ok, data, nextLink?, deltaLink?, count?}` envelope, unambiguous for
82
82
  "hint": "The ID you passed isn't valid for this endpoint. Source IDs from a sibling `list-*` command…",
83
83
  "source": "graph"
84
84
  }
85
+
86
+ // Error — throttled (HTTP 429 / 503): `retryAfterSeconds` carries Graph's `Retry-After`
87
+ {
88
+ "ok": false,
89
+ "error": "TooManyRequests: Too many requests",
90
+ "errorCode": "TooManyRequests",
91
+ "source": "graph",
92
+ "retryAfterSeconds": 120
93
+ }
85
94
  ```
86
95
 
87
96
  `@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.
88
97
 
89
- `source` is one of `graph` | `substrate` | `cli` | `validation`. `hint` is present when a curated rule matched the error code or message; the envelope shape is `{ok, error, errorCode?, hint?, source}` where only `hint` is conditional.
98
+ `source` is one of `graph` | `substrate` | `cli` | `validation`. `hint` is present when a curated rule matched the error code or message; the envelope shape is `{ok, error, errorCode?, hint?, source, retryAfterSeconds?}` where `hint` and `retryAfterSeconds` are conditional. `retryAfterSeconds` appears only when Graph returned a `Retry-After` header (throttling: 429, sometimes 503) and is the integer seconds to wait before retrying — honor it instead of guessing a backoff. In text output the same value renders as a `retryAfter: Ns` line. It is omitted when the header is absent or in the rarely-seen HTTP-date form (delta-seconds only).
90
99
 
91
100
  ## OData query passthrough
92
101
 
@@ -139,7 +148,7 @@ ask-marcel convert-mail-attachment-to-pdf \
139
148
  # {"ok":true,"data":{"contentType":"application/pdf","size":4837291,"savedTo":"/tmp/deck.pdf"}}
140
149
  ```
141
150
 
142
- `--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.
151
+ `--output-path` decodes `base64` (or writes `text`) to the path and replaces the inline field with `savedTo: <path>` in the response — stripping **every** raw-byte field, including the `contentBytes` mirror on `get-mail-attachment`, so stdout stays a compact metadata envelope regardless of payload size. 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.
143
152
 
144
153
  `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.
145
154
 
@@ -185,6 +194,14 @@ const graph = createGraphClient({
185
194
  const me = await commands['get-current-user'].execute(graph, {});
186
195
  ```
187
196
 
197
+ One special case: `convert-local-file` and `extract-local-file-images` read the **local filesystem**, not Graph — their registry-typed `execute` returns a redirect error, and the real entry point is the optional `executeLocal(fs, params)` on the same command object (the CLI wires this automatically; library consumers pass their own `FileSystem`):
198
+
199
+ ```ts
200
+ import { commands, createNodeFileSystem } from 'ask-marcel-office-cli';
201
+ const md = await commands['convert-local-file'].executeLocal?.(createNodeFileSystem(), { path: './report.docx' });
202
+ const images = await commands['extract-local-file-images'].executeLocal?.(createNodeFileSystem(), { path: './deck.pdf' });
203
+ ```
204
+
188
205
  The full export list (registry, factories, `Result`, branded types, ports) is in [`src/index.ts`](../src/index.ts). The machine-readable manifest is also available as a JSON subpath import:
189
206
 
190
207
  ```ts
@@ -206,7 +223,7 @@ src/
206
223
  - **Auth**: Three-rung recovery ladder — file-based cached JWT → OAuth refresh_token exchange → Playwright browser intercepting Teams login
207
224
  - **Client ID**: `5e3ce6c0-2b1f-4285-8d4b-75ee78787346` (Teams Web)
208
225
  - **Scopes**: `https://graph.microsoft.com/.default openid profile offline_access`
209
- - **Token cache**: `~/.ask-marcel/token-cache.json` (overridable via `BuildDepsConfig.cachePath`)
226
+ - **Token cache**: `~/.ask-marcel/token-cache.json`, written `0600` (overridable via `BuildDepsConfig.cachePath`)
210
227
  - **Browser profile**: `~/.ask-marcel/browser-profile` (overridable via `ASKMARCEL_BROWSER_PROFILE`)
211
228
  - **Output**: YAML-ish text by default (LLM-readable, generally smaller than the JSON envelope on long listings, parity on small projected pages); compact JSON envelope via `--output json` for tool-chaining and `jq` pipelines
212
229
 
@@ -231,7 +248,7 @@ Environment variables read at composition time:
231
248
  ## Quality gates (atelier four-check loop)
232
249
 
233
250
  ```bash
234
- bun test # full suite (2800+ tests)
251
+ bun test # full suite (3700+ tests)
235
252
  bun run lint # ESLint (0 warnings, 0 errors)
236
253
  bun run typecheck # tsc --noEmit
237
254
  bun run coverage # per-tier gates (100% on every tier: domain, use-cases, infra, composition, presenter)