ask-marcel-office-cli 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +68 -24
  3. package/dist/cli.js +7631 -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 +10421 -93852
  11. package/dist/infra/auth.d.ts +26 -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
@@ -1,11 +1,162 @@
1
1
  {
2
2
  "package": "ask-marcel-office-cli",
3
- "version": "1.4.0",
4
- "generatedAt": "2026-05-27T00:44:06.845Z",
3
+ "version": "1.5.0",
4
+ "generatedAt": "2026-06-16T16:14:42.738Z",
5
5
  "commands": [
6
+ {
7
+ "name": "convert-calendar-event-attachment-to-markdown",
8
+ "summary": "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.",
9
+ "category": "calendar",
10
+ "graphMethod": "GET",
11
+ "graphPathTemplate": "/me/events/{event-id}/attachments/{attachment-id}",
12
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
13
+ "options": [
14
+ {
15
+ "name": "event-id",
16
+ "key": "eventId",
17
+ "required": true,
18
+ "description": "Outlook calendar event ID. Returned by `list-calendar-events` or `get-calendar-event`."
19
+ },
20
+ {
21
+ "name": "attachment-id",
22
+ "key": "attachmentId",
23
+ "required": true,
24
+ "description": "Attachment ID inside that event. Returned by `list-calendar-event-attachments`."
25
+ },
26
+ {
27
+ "name": "include-metadata",
28
+ "key": "includeMetadata",
29
+ "required": false,
30
+ "description": "Pass `--include-metadata true` to surface side-channel content for docx, xlsx, pptx, and OpenDocument attachments. docx → `## DOCX metadata`; xlsx → `## Workbook metadata`; pptx → `## PPTX metadata` (standalone, since pptx has no convertible body); odt/ods/odp → `## OpenDocument metadata`, appended after the converted body. No-op on other attachment types and on itemAttachment renderers.",
31
+ "argumentHint": {
32
+ "kind": "magicValue",
33
+ "values": [
34
+ "true",
35
+ "false"
36
+ ]
37
+ }
38
+ }
39
+ ],
40
+ "example": "ask-marcel convert-calendar-event-attachment-to-markdown --event-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1'",
41
+ "responseShape": "`{ contentType: \"text/markdown\", size, text }` on success (file/reference attachments converted via Graph + turndown; itemAttachment rendered locally). Plain-text source extensions return the raw-bytes envelope; unsupported types return an api_error with status 400.",
42
+ "producesBytes": true
43
+ },
44
+ {
45
+ "name": "convert-calendar-event-attachment-to-pdf",
46
+ "summary": "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.",
47
+ "category": "calendar",
48
+ "graphMethod": "GET",
49
+ "graphPathTemplate": "/me/events/{event-id}/attachments/{attachment-id}",
50
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
51
+ "options": [
52
+ {
53
+ "name": "event-id",
54
+ "key": "eventId",
55
+ "required": true,
56
+ "description": "Outlook calendar event ID. Returned by `list-calendar-events` or `get-calendar-event`."
57
+ },
58
+ {
59
+ "name": "attachment-id",
60
+ "key": "attachmentId",
61
+ "required": true,
62
+ "description": "Attachment ID inside that event. Returned by `list-calendar-event-attachments`."
63
+ }
64
+ ],
65
+ "example": "ask-marcel convert-calendar-event-attachment-to-pdf --event-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1' --output-path ./deck.pdf",
66
+ "responseShape": "`{ contentType: \"application/pdf\", size, base64 }` — the PDF bytes, inlined. Plain-text and pdf sources short-circuit to `{ contentType, size, base64, note }`; image attachments return api_error 415; itemAttachment returns api_error 400. Pair with the global `--output-path` to land the bytes on disk and replace `base64` with `savedTo`.",
67
+ "producesBytes": true
68
+ },
69
+ {
70
+ "name": "convert-drive-item-zip",
71
+ "summary": "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`.",
72
+ "category": "drive",
73
+ "graphMethod": "GET",
74
+ "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content",
75
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get-content",
76
+ "options": [
77
+ {
78
+ "name": "drive-id",
79
+ "key": "driveId",
80
+ "required": true,
81
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
82
+ },
83
+ {
84
+ "name": "item-id",
85
+ "key": "itemId",
86
+ "required": true,
87
+ "description": "driveItem ID of the .zip file. Returned by `list-folder-files` or `search-onedrive-files`."
88
+ },
89
+ {
90
+ "name": "include-metadata",
91
+ "key": "includeMetadata",
92
+ "required": false,
93
+ "description": "Pass `--include-metadata true` to append each converted Office file’s side-channel metadata block (`## DOCX metadata` / `## Workbook metadata` / `## PPTX metadata` / `## OpenDocument metadata`, etc.) after its body.",
94
+ "argumentHint": {
95
+ "kind": "magicValue",
96
+ "values": [
97
+ "true",
98
+ "false"
99
+ ]
100
+ }
101
+ }
102
+ ],
103
+ "example": "ask-marcel convert-drive-item-zip --drive-id 'b!1234' --item-id '01ABC'",
104
+ "responseShape": "`{ count, files: [{ path, contentType, size, text }] }` — one entry per file in the archive (sorted by path). Convertible files carry `{ contentType, size, text }` (the markdown); unsupported / failed entries carry `{ path, note }` instead. When the archive has more than 100 entries the response adds `truncated: true` + `totalEntries` and only the first 100 are converted."
105
+ },
106
+ {
107
+ "name": "convert-local-file",
108
+ "summary": "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.",
109
+ "category": "meta",
110
+ "graphMethod": "GET",
111
+ "graphPathTemplate": "(local) reads {path} from the local filesystem; not a Graph endpoint",
112
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/",
113
+ "options": [
114
+ {
115
+ "name": "path",
116
+ "key": "path",
117
+ "required": true,
118
+ "description": "Filesystem path of the file to convert (absolute, or relative to the current working directory). E.g. `./report.docx`, `/tmp/handover.zip`."
119
+ },
120
+ {
121
+ "name": "include-metadata",
122
+ "key": "includeMetadata",
123
+ "required": false,
124
+ "description": "Pass `--include-metadata true` to append the converted Office file’s side-channel metadata block (`## DOCX metadata` / `## Workbook metadata` / `## PPTX metadata` / `## OpenDocument metadata`, etc.) after its body. Applies inside a `.zip` too.",
125
+ "argumentHint": {
126
+ "kind": "magicValue",
127
+ "values": [
128
+ "true",
129
+ "false"
130
+ ]
131
+ }
132
+ },
133
+ {
134
+ "name": "inline-images",
135
+ "key": "inlineImages",
136
+ "required": false,
137
+ "description": "Pass `--inline-images true` to embed a docx's images as base64 `data:` URIs. Default `false` — each image becomes an `[image: <alt>]` placeholder. No-op on non-docx sources.",
138
+ "argumentHint": {
139
+ "kind": "magicValue",
140
+ "values": [
141
+ "true",
142
+ "false"
143
+ ]
144
+ }
145
+ },
146
+ {
147
+ "name": "max-cells",
148
+ "key": "maxCells",
149
+ "required": false,
150
+ "description": "Per-sheet cell cap (positive integer; default 50 000) for xlsx/csv sources. A sheet whose used range exceeds the cap renders as a truncation hint instead of a multi-hundred-MB table. No-op on other sources."
151
+ }
152
+ ],
153
+ "example": "ask-marcel convert-local-file --path ./report.docx",
154
+ "responseShape": "`{ contentType: \"text/markdown\" | \"text/plain\", size, text }` for a single file; `{ count, files: [{ path, contentType, size, text } | { path, note }] }` for a `.zip` (one entry per contained file, unsupported entries noted). A missing file returns api_error 404 with the path. Pair with the global `--output-path` to land the markdown on disk.",
155
+ "producesBytes": true
156
+ },
6
157
  {
7
158
  "name": "convert-mail-attachment-to-markdown",
8
- "summary": "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).",
159
+ "summary": "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).",
9
160
  "category": "mail",
10
161
  "graphMethod": "GET",
11
162
  "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
@@ -22,10 +173,24 @@
22
173
  "key": "attachmentId",
23
174
  "required": true,
24
175
  "description": "Attachment ID inside that message. Returned by `list-mail-attachments`."
176
+ },
177
+ {
178
+ "name": "include-metadata",
179
+ "key": "includeMetadata",
180
+ "required": false,
181
+ "description": "Pass `--include-metadata true` to surface side-channel content for docx, xlsx, pptx, and OpenDocument attachments (file + reference). docx → `## DOCX metadata` (properties, people, hyperlinks, comments, tracked changes, hidden text, fields, bookmarks); xlsx → `## Workbook metadata` (properties, external relationships, defined names, hidden / very-hidden sheets, cell + threaded comments, persons); pptx → `## PPTX metadata` (properties, external relationships, slide tags, comment authors + comments, per-slide title / speaker notes / hidden flag) as a standalone document, since pptx has no convertible body; odt/ods/odp → `## OpenDocument metadata` (Dublin Core + ODF properties, keywords, user-defined fields), appended after the converted body. Each OOXML family also covers its macro-enabled and template variants, with a `### Macros (VBA)` section flagging an embedded `vbaProject.bin`. No-op on other attachment types and on itemAttachment renderers.",
182
+ "argumentHint": {
183
+ "kind": "magicValue",
184
+ "values": [
185
+ "true",
186
+ "false"
187
+ ]
188
+ }
25
189
  }
26
190
  ],
27
191
  "example": "ask-marcel convert-mail-attachment-to-markdown --message-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1'",
28
- "responseShape": "`{ contentType: \"text/markdown\", size, text }` on success (file/reference attachments converted via Graph + turndown; itemAttachment rendered locally). Plain-text source extensions return the raw-bytes envelope; unsupported types return an api_error with status 400."
192
+ "responseShape": "`{ contentType: \"text/markdown\", size, text }` on success (file/reference attachments converted via Graph + turndown; itemAttachment rendered locally). Plain-text source extensions return the raw-bytes envelope; unsupported types return an api_error with status 400.",
193
+ "producesBytes": true
29
194
  },
30
195
  {
31
196
  "name": "convert-mail-attachment-to-pdf",
@@ -49,7 +214,45 @@
49
214
  }
50
215
  ],
51
216
  "example": "ask-marcel convert-mail-attachment-to-pdf --message-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1'",
52
- "responseShape": "`{ contentType: \"application/pdf\", size, base64 }` — the PDF bytes, inlined. The CLI follows the SharePoint media-transform redirect internally so the LLM never has to fetch an external URL. Plain-text source extensions and pdf sources short-circuit to `{ contentType, size, base64, note }` with their native bytes; itemAttachment returns api_error 400. Pair with the global `--output-path` to land the bytes on disk and replace `base64` with `savedTo` for multi-MB PDFs."
217
+ "responseShape": "`{ contentType: \"application/pdf\", size, base64 }` — the PDF bytes, inlined. The CLI follows the SharePoint media-transform redirect internally so the LLM never has to fetch an external URL. Plain-text source extensions and pdf sources short-circuit to `{ contentType, size, base64, note }` with their native bytes; itemAttachment returns api_error 400. Pair with the global `--output-path` to land the bytes on disk and replace `base64` with `savedTo` for multi-MB PDFs.",
218
+ "producesBytes": true
219
+ },
220
+ {
221
+ "name": "convert-mail-attachment-zip",
222
+ "summary": "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).",
223
+ "category": "mail",
224
+ "graphMethod": "GET",
225
+ "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
226
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
227
+ "options": [
228
+ {
229
+ "name": "message-id",
230
+ "key": "messageId",
231
+ "required": true,
232
+ "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
233
+ },
234
+ {
235
+ "name": "attachment-id",
236
+ "key": "attachmentId",
237
+ "required": true,
238
+ "description": "Attachment ID of the .zip fileAttachment. Returned by `list-mail-attachments`."
239
+ },
240
+ {
241
+ "name": "include-metadata",
242
+ "key": "includeMetadata",
243
+ "required": false,
244
+ "description": "Pass `--include-metadata true` to append each converted Office file’s side-channel metadata block (`## DOCX metadata` / `## Workbook metadata` / `## PPTX metadata` / `## OpenDocument metadata`, etc.) after its body.",
245
+ "argumentHint": {
246
+ "kind": "magicValue",
247
+ "values": [
248
+ "true",
249
+ "false"
250
+ ]
251
+ }
252
+ }
253
+ ],
254
+ "example": "ask-marcel convert-mail-attachment-zip --message-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1'",
255
+ "responseShape": "`{ count, files: [{ path, contentType, size, text }] }` — one entry per file in the archive (sorted by path; non-mojibake names). Convertible files carry `{ contentType, size, text }` (the markdown); unsupported / failed entries carry `{ path, note }`. When the archive has more than 100 entries the response adds `truncated: true` + `totalEntries` and only the first 100 are converted. A non-fileAttachment (itemAttachment / referenceAttachment) or a non-zip payload returns an api_error."
53
256
  },
54
257
  {
55
258
  "name": "convert-mail-to-markdown",
@@ -63,21 +266,121 @@
63
266
  "name": "message-id",
64
267
  "key": "messageId",
65
268
  "required": true,
66
- "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
269
+ "aliases": [
270
+ {
271
+ "name": "id",
272
+ "key": "id"
273
+ }
274
+ ],
275
+ "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`. Accepts `--id` as an alias."
67
276
  },
68
277
  {
69
278
  "name": "inline-images",
70
279
  "key": "inlineImages",
71
280
  "required": false,
72
281
  "description": "Pass `--inline-images false` to skip the per-image bytes fetch + base64 embedding. Default is `true` (embed). Disabling cuts the response size dramatically on emails with several inline images (a 6 KB body with 6 inline images shipped at ~36 KB by default; with `--inline-images false` it stays close to 6 KB). The body keeps raw `cid:<contentId>` references and the inline images surface in the file-attachments list instead, so the LLM caller can still see what is there and fetch any specific image via `get-mail-attachment` on demand."
282
+ },
283
+ {
284
+ "name": "keep-quoted",
285
+ "key": "keepQuoted",
286
+ "required": false,
287
+ "description": "Quoted reply chains and forwarded-message blocks are stripped by default (they duplicate content already present in earlier messages and inflate the context budget). The stripped tail is replaced with a single visible marker naming this flag, so nothing is removed silently. Pass `--keep-quoted true` to preserve the full body. Only well-known structural markers are cut: in HTML bodies Outlook `divRplyFwdMsg` / `appendonsend` / `stopSpelling` and Gmail `gmail_quote`; in plain-text bodies the `Original Message` banner, the `On … wrote:` attribution line, and leading `>` quote lines.",
288
+ "argumentHint": {
289
+ "kind": "magicValue",
290
+ "values": [
291
+ "true",
292
+ "false"
293
+ ]
294
+ }
73
295
  }
74
296
  ],
75
297
  "example": "ask-marcel convert-mail-to-markdown --message-id 'AAMkAD...'",
76
- "responseShape": "`{ contentType: \"text/markdown\", size, text, note? }` — headers + turndown-rendered body + (when present) a file-attachments list. The optional `note` carries a partial-success hint when the attachments-metadata fetch fails after the body succeeded."
298
+ "responseShape": "`{ contentType: \"text/markdown\", size, text, note? }` — headers + turndown-rendered body + (when present) a file-attachments list. The optional `note` carries a partial-success hint when the attachments-metadata fetch fails after the body succeeded, and/or a flag that a quoted reply chain was stripped (use `--keep-quoted true` to include it).",
299
+ "producesBytes": true
300
+ },
301
+ {
302
+ "name": "create-mail-draft",
303
+ "summary": "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.",
304
+ "category": "mail",
305
+ "graphMethod": "POST",
306
+ "graphPathTemplate": "/me/messages (or /me/mailFolders/{mail-folder-id}/messages)",
307
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-post-messages",
308
+ "options": [
309
+ {
310
+ "name": "subject",
311
+ "key": "subject",
312
+ "required": true,
313
+ "description": "Email subject line."
314
+ },
315
+ {
316
+ "name": "body-content",
317
+ "key": "bodyContent",
318
+ "required": true,
319
+ "description": "Email body content. Plain text by default; pass --body-content-type HTML for rich text."
320
+ },
321
+ {
322
+ "name": "body-content-type",
323
+ "key": "bodyContentType",
324
+ "required": false,
325
+ "description": "Body format: Text (default) or HTML.",
326
+ "argumentHint": {
327
+ "kind": "magicValue",
328
+ "values": [
329
+ "Text",
330
+ "HTML"
331
+ ]
332
+ }
333
+ },
334
+ {
335
+ "name": "to-recipients",
336
+ "key": "toRecipients",
337
+ "required": true,
338
+ "description": "Comma-separated list of recipient email addresses (e.g. \"alice@example.com,bob@example.com\")."
339
+ },
340
+ {
341
+ "name": "cc-recipients",
342
+ "key": "ccRecipients",
343
+ "required": false,
344
+ "description": "Comma-separated list of CC recipient email addresses."
345
+ },
346
+ {
347
+ "name": "bcc-recipients",
348
+ "key": "bccRecipients",
349
+ "required": false,
350
+ "description": "Comma-separated list of BCC recipient email addresses."
351
+ },
352
+ {
353
+ "name": "importance",
354
+ "key": "importance",
355
+ "required": false,
356
+ "description": "Email importance: Low, Normal (default), or High.",
357
+ "argumentHint": {
358
+ "kind": "magicValue",
359
+ "values": [
360
+ "Low",
361
+ "Normal",
362
+ "High"
363
+ ]
364
+ }
365
+ },
366
+ {
367
+ "name": "mail-folder-id",
368
+ "key": "mailFolderId",
369
+ "required": false,
370
+ "description": "Target folder ID to create the draft in. Defaults to the Drafts folder. Source from list-mail-folders.",
371
+ "argumentHint": {
372
+ "kind": "idOrName"
373
+ }
374
+ }
375
+ ],
376
+ "example": "ask-marcel create-mail-draft --subject \"Q3 Report\" --body-content \"Please review the attached report.\" --to-recipients \"alice@example.com,bob@example.com\" --importance High",
377
+ "responseShape": "The created Microsoft Graph message object: `{ id, subject, body, from, toRecipients, ccRecipients, bccRecipients, receivedDateTime, isDraft, … }`. The `id` field is the draft message ID — use it with `update-mail-draft` to modify before sending.",
378
+ "bodyTemplate": "{ subject: '{subject}', body: { contentType: '{body-content-type}', content: '{body-content}' }, toRecipients: '{to-recipients}', ccRecipients: '{cc-recipients}', bccRecipients: '{bcc-recipients}', importance: '{importance}' }",
379
+ "mutates": true
77
380
  },
78
381
  {
79
382
  "name": "download-drive-item-as-markdown",
80
- "summary": "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.",
383
+ "summary": "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.",
81
384
  "category": "drive",
82
385
  "graphMethod": "GET",
83
386
  "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content?format=html",
@@ -94,14 +397,47 @@
94
397
  "key": "itemId",
95
398
  "required": true,
96
399
  "description": "driveItem ID of the file to convert. Returned by `list-folder-files` or `search-onedrive-files`."
400
+ },
401
+ {
402
+ "name": "include-metadata",
403
+ "key": "includeMetadata",
404
+ "required": false,
405
+ "description": "Pass `--include-metadata true` to surface the side-channel content the rendered body hides. For docx (`## DOCX metadata`): core/app/custom document properties, people registry, external hyperlinks, comments, tracked changes (insertions + deletions), hidden-formatted text (w:vanish), field instructions (MERGEFIELD / HYPERLINK / DOCVARIABLE), bookmarks. For xlsx (`## Workbook metadata`): core/app/custom properties, external relationships, defined names, hidden / very-hidden sheets, legacy cell comments, threaded comments, persons. For pptx (`## PPTX metadata`): properties, external relationships, slide tags, comment authors + comments (legacy + modern), and per-slide title / speaker notes / hidden flag — returned as a standalone document since pptx has no convertible body (use `download-drive-item-as-pdf` for slide visuals). For OpenDocument (`.odt`/`.ods`/`.odp`, `## OpenDocument metadata`): Dublin Core + ODF properties, keywords, user-defined custom fields — appended after the converted body. Each OOXML family also covers its macro-enabled (`.docm` / `.xlsm` / `.pptm`) and template (`.dotx` / `.xltx` / `.potx`, etc.) variants, with a `### Macros (VBA)` section flagging an embedded `vbaProject.bin`. No-op on other sources.",
406
+ "argumentHint": {
407
+ "kind": "magicValue",
408
+ "values": [
409
+ "true",
410
+ "false"
411
+ ]
412
+ }
413
+ },
414
+ {
415
+ "name": "inline-images",
416
+ "key": "inlineImages",
417
+ "required": false,
418
+ "description": "Pass `--inline-images true` to embed a docx's images as base64 `data:` URIs (self-contained markdown). Default is `false` — each image becomes an `[image: <alt>]` placeholder that keeps its position in the text without the base64 bloat (a single embedded photo can dwarf the prose). To get the actual pictures, run `extract-drive-item-images` on the same drive/item — it returns the full-resolution, un-cropped originals as files (via `--output-dir`). No-op on non-docx sources (xlsx/csv/odf have no inline images).",
419
+ "argumentHint": {
420
+ "kind": "magicValue",
421
+ "values": [
422
+ "true",
423
+ "false"
424
+ ]
425
+ }
426
+ },
427
+ {
428
+ "name": "max-cells",
429
+ "key": "maxCells",
430
+ "required": false,
431
+ "description": "Per-sheet cell cap (positive integer; default 50 000) for xlsx sources. A worksheet whose used range (rows × cols) exceeds the cap is rendered as its `## SheetName` header plus a one-line hint pointing at `get-excel-used-range` / `get-excel-range` for band-by-band reads, instead of a full markdown table — a genuinely dense 49 MB workbook otherwise builds a multi-hundred-MB string and OOMs the process. Raise it to force a larger render. No-op on non-xlsx sources."
97
432
  }
98
433
  ],
99
434
  "example": "ask-marcel download-drive-item-as-markdown --drive-id 'b!1234' --item-id '01ABC'",
100
- "responseShape": "`{ contentType: \"text/markdown\", size: <chars>, text: \"...\" }` for the locally-converted case (docx/xlsx/csv); `{ contentType: \"text/plain\", size, text }` for plain-text passthrough sources (txt/md/html/etc.) — bytes are inlined whether Graph returns them directly or via a CDN redirect that the CLI follows internally."
435
+ "responseShape": "`{ contentType: \"text/markdown\", size: <chars>, text: \"...\" }` for the locally-converted case (docx/xlsx/csv/.msg); `{ contentType: \"text/plain\", size, text }` for plain-text passthrough sources (txt/md/html/etc.) — bytes are inlined whether Graph returns them directly or via a CDN redirect that the CLI follows internally.",
436
+ "producesBytes": true
101
437
  },
102
438
  {
103
439
  "name": "download-drive-item-as-pdf",
104
- "summary": "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.",
440
+ "summary": "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.",
105
441
  "category": "drive",
106
442
  "graphMethod": "GET",
107
443
  "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content?format=pdf",
@@ -121,11 +457,40 @@
121
457
  }
122
458
  ],
123
459
  "example": "ask-marcel download-drive-item-as-pdf --drive-id 'b!1234' --item-id '01ABC'",
124
- "responseShape": "`{ contentType: \"application/pdf\", size, base64 }` — the PDF bytes, inlined. The CLI follows the SharePoint media-transform redirect internally so the LLM never has to fetch an external URL. Plain-text and pdf sources skip the format=pdf round-trip and return the raw file bytes under the same envelope shape (with their native contentType) plus `passthrough: true` and a `note` explaining why conversion was skipped — the LLM can branch on the flag if it cares whether Graph actually converted. Pair with the global `--output-path` to land the bytes on disk and replace `base64` with `savedTo` for multi-MB PDFs."
460
+ "responseShape": "`{ contentType: \"application/pdf\", size, base64 }` — the PDF bytes, inlined. The CLI follows the SharePoint media-transform redirect internally so the LLM never has to fetch an external URL. Plain-text and pdf sources skip the format=pdf round-trip and return the raw file bytes under the same envelope shape (with their native contentType) plus `passthrough: true` and a `note` explaining why conversion was skipped — the LLM can branch on the flag if it cares whether Graph actually converted. Pair with the global `--output-path` to land the bytes on disk and replace `base64` with `savedTo` for multi-MB PDFs.",
461
+ "producesBytes": true
462
+ },
463
+ {
464
+ "name": "download-drive-item-content",
465
+ "summary": "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.",
466
+ "category": "drive",
467
+ "commandAliases": [
468
+ "download-onedrive-file-content"
469
+ ],
470
+ "graphMethod": "GET",
471
+ "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content",
472
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get-content",
473
+ "options": [
474
+ {
475
+ "name": "drive-id",
476
+ "key": "driveId",
477
+ "required": true,
478
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
479
+ },
480
+ {
481
+ "name": "item-id",
482
+ "key": "itemId",
483
+ "required": true,
484
+ "description": "driveItem ID of the file to download. Returned by `ask-marcel list-folder-files` (works on SharePoint library drives too) or `search-onedrive-files`."
485
+ }
486
+ ],
487
+ "example": "ask-marcel download-drive-item-content --drive-id 'b!1234' --item-id '01ABC'",
488
+ "responseShape": "`{ contentType: \"text/plain\", size, text }` when the bytes decode as valid UTF-8; `{ contentType, size, base64 }` otherwise (binary, or non-UTF-8-encoded text). Pair with the global `--output-path <path>` flag to land the bytes on disk and replace the inline field with `savedTo` for multi-MB files.",
489
+ "producesBytes": true
125
490
  },
126
491
  {
127
492
  "name": "download-drive-item-version",
128
- "summary": "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).",
493
+ "summary": "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).",
129
494
  "category": "drive",
130
495
  "graphMethod": "GET",
131
496
  "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/versions/{version-id}/content",
@@ -153,7 +518,7 @@
153
518
  "name": "format",
154
519
  "key": "format",
155
520
  "required": false,
156
- "description": "Output format. `original` (default) returns the raw historical-version bytes. `pdf` runs Graph `?format=pdf` for Office sources (docx/pptx/xlsx) — plain-text and pdf sources short-circuit to raw bytes with `passthrough: true`. `markdown` runs the local conversion pipeline (mammoth/sheetjs/csv/plain-text). All formats inline the bytes; pair with the global `--output-path` to land them on disk.",
521
+ "description": "Output format. `original` (default) returns the raw historical-version bytes. `pdf` runs Graph `?format=pdf` for Office sources (docx/pptx/xlsx) — plain-text and pdf sources short-circuit to raw bytes with `passthrough: true`. `markdown` runs the local conversion pipeline (mammoth/sheetjs/csv/odf/plain-text). All formats inline the bytes; pair with the global `--output-path` to land them on disk.",
157
522
  "argumentHint": {
158
523
  "kind": "magicValue",
159
524
  "values": [
@@ -162,14 +527,29 @@
162
527
  "markdown"
163
528
  ]
164
529
  }
530
+ },
531
+ {
532
+ "name": "include-metadata",
533
+ "key": "includeMetadata",
534
+ "required": false,
535
+ "description": "Pass `--include-metadata true` to surface side-channel content (only meaningful with `--format markdown` AND a docx / xlsx / pptx / odt / ods / odp source — silently ignored otherwise). docx → `## DOCX metadata` (properties, people, hyperlinks, comments, tracked changes, hidden text, fields, bookmarks); xlsx → `## Workbook metadata` (properties, external relationships, defined names, hidden / very-hidden sheets, cell + threaded comments, persons); pptx → `## PPTX metadata` (properties, external relationships, slide tags, comment authors + comments, per-slide title / speaker notes / hidden flag); odt/ods/odp → `## OpenDocument metadata` (Dublin Core + ODF properties, keywords, user-defined fields). Each OOXML family covers its macro-enabled and template variants too, with a `### Macros (VBA)` section flagging an embedded `vbaProject.bin`.",
536
+ "argumentHint": {
537
+ "kind": "magicValue",
538
+ "values": [
539
+ "true",
540
+ "false"
541
+ ]
542
+ }
165
543
  }
166
544
  ],
167
545
  "example": "ask-marcel download-drive-item-version --drive-id 'b!1234' --item-id '01ABC' --version-id '4.0' --format pdf",
168
- "responseShape": "`--format original` & `--format pdf`: `{ contentType, size, base64 }` — the bytes, inlined. `--format pdf` adds `passthrough: true` + `note` when Graph short-circuits (plain-text or pdf source) OR silently falls back to raw source bytes — in that case save with the source extension, NOT `.pdf` (the global output-path flag refuses the mismatch). `--format markdown`: `{ contentType: \"text/markdown\", size: <chars>, text: \"...\" }` for the converted case; raw-bytes envelope for plain-text source extensions. Pair with the global `--output-path` to land bytes on disk and replace `base64`/`text` with `savedTo` for multi-MB versions."
546
+ "responseShape": "`--format original` & `--format pdf`: `{ contentType, size, base64 }` — the bytes, inlined. `--format pdf` adds `passthrough: true` + `note` when Graph short-circuits (plain-text or pdf source) OR silently falls back to raw source bytes — in that case save with the source extension, NOT `.pdf` (the global output-path flag refuses the mismatch). `--format markdown`: `{ contentType: \"text/markdown\", size: <chars>, text: \"...\" }` for the converted case; raw-bytes envelope for plain-text source extensions. Pair with the global `--output-path` to land bytes on disk and replace `base64`/`text` with `savedTo` for multi-MB versions.",
547
+ "producesBytes": true,
548
+ "needsElevatedToken": true
169
549
  },
170
550
  {
171
- "name": "download-onedrive-file-content",
172
- "summary": "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.",
551
+ "name": "extract-drive-item-images",
552
+ "summary": "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`.",
173
553
  "category": "drive",
174
554
  "graphMethod": "GET",
175
555
  "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content",
@@ -179,17 +559,86 @@
179
559
  "name": "drive-id",
180
560
  "key": "driveId",
181
561
  "required": true,
182
- "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
562
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
183
563
  },
184
564
  {
185
565
  "name": "item-id",
186
566
  "key": "itemId",
187
567
  "required": true,
188
- "description": "driveItem ID of the file to download. Returned by `ask-marcel list-folder-files` or `search-onedrive-files`."
568
+ "description": "driveItem ID of the pdf / docx / xlsx / pptx file. Returned by `list-folder-files` or `search-onedrive-files`."
569
+ }
570
+ ],
571
+ "example": "ask-marcel extract-drive-item-images --drive-id 'b!1234' --item-id '01ABC' --output-dir ./deck-images",
572
+ "responseShape": "`{ count, media: [{ path, contentType, sizeBytes, base64 }] }`. `path` is the source part path — `ppt/media/image3.png` for OOXML, `pdf/page2/<key>.png` for PDF (every PDF image is re-encoded as PNG). Pair with the global `--output-dir <dir>` to write each image to that folder — the response then replaces each `base64` with `savedTo: <dir>/<filename>` (the part path is flattened, e.g. `pdf_page2_Im0.png`). `count: 0` with an empty `media` array means the document embeds no extractable images (after the emf/wmf/audio/video filter).",
573
+ "producesMedia": true
574
+ },
575
+ {
576
+ "name": "extract-local-file-images",
577
+ "summary": "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.",
578
+ "category": "meta",
579
+ "graphMethod": "GET",
580
+ "graphPathTemplate": "(local) reads {path} from the local filesystem; not a Graph endpoint",
581
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/",
582
+ "options": [
583
+ {
584
+ "name": "path",
585
+ "key": "path",
586
+ "required": true,
587
+ "description": "Filesystem path of the pdf / docx / xlsx / pptx file to extract from (absolute, or relative to the current working directory). E.g. `/tmp/deck.pdf`, `./report.docx`."
189
588
  }
190
589
  ],
191
- "example": "ask-marcel download-onedrive-file-content --drive-id 'b!1234' --item-id '01ABC'",
192
- "responseShape": "`{ contentType: \"text/plain\", size, text }` for plain-text source extensions; `{ contentType, size, base64 }` for everything else. Pair with the global `--output-path <path>` flag to land the bytes on disk and replace the inline field with `savedTo` for multi-MB files."
590
+ "example": "ask-marcel extract-local-file-images --path /tmp/deck.pdf --output-dir ./deck-images",
591
+ "responseShape": "`{ count, media: [{ path, contentType, sizeBytes, base64 }] }`. `path` is the source part path — `ppt/media/image3.png` for OOXML, `pdf/page2/<key>.png` for PDF (every PDF image is re-encoded as PNG). Pair with the global `--output-dir <dir>` to write each image to that folder — the response then replaces each `base64` with `savedTo: <dir>/<filename>` (the part path is flattened, e.g. `pdf_page2_Im0.png`). `count: 0` with an empty `media` array means the document embeds no extractable images (after the emf/wmf/audio/video filter). A missing file returns api_error 404 with the path.",
592
+ "producesMedia": true
593
+ },
594
+ {
595
+ "name": "extract-mail-attachment-images",
596
+ "summary": "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.",
597
+ "category": "mail",
598
+ "graphMethod": "GET",
599
+ "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
600
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
601
+ "options": [
602
+ {
603
+ "name": "message-id",
604
+ "key": "messageId",
605
+ "required": true,
606
+ "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
607
+ },
608
+ {
609
+ "name": "attachment-id",
610
+ "key": "attachmentId",
611
+ "required": true,
612
+ "description": "Attachment ID inside that message. Returned by `list-mail-attachments`."
613
+ }
614
+ ],
615
+ "example": "ask-marcel extract-mail-attachment-images --message-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1' --output-dir ./att-images",
616
+ "responseShape": "`{ count, media: [{ path, contentType, sizeBytes, base64 }] }`. `path` is the in-package part path (e.g. `ppt/media/image3.png`). Pair with the global `--output-dir <dir>` to write each image to that folder — the response then replaces each `base64` with `savedTo` (the part path is flattened, e.g. `pdf_page2_Im0.png`). `count: 0` means the attachment embeds no extractable images (after the emf/wmf/audio/video filter).",
617
+ "producesMedia": true
618
+ },
619
+ {
620
+ "name": "extract-sharepoint-links-in-documents",
621
+ "summary": "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.",
622
+ "category": "drive",
623
+ "graphMethod": "GET",
624
+ "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content",
625
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get-content",
626
+ "options": [
627
+ {
628
+ "name": "drive-id",
629
+ "key": "driveId",
630
+ "required": true,
631
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
632
+ },
633
+ {
634
+ "name": "item-id",
635
+ "key": "itemId",
636
+ "required": true,
637
+ "description": "driveItem ID of the .docx/.xlsx/.pptx or .odt/.ods/.odp file. Returned by `list-folder-files` or `search-onedrive-files`."
638
+ }
639
+ ],
640
+ "example": "ask-marcel extract-sharepoint-links-in-documents --drive-id 'b!1234' --item-id '01ABC'",
641
+ "responseShape": "`{ driveId, itemId, links: [{ url, driveId, itemId, name, webUrl } | { url, error }], truncated, skippedCount }` — one entry per unique SharePoint URL found in the document’s external relationships."
193
642
  },
194
643
  {
195
644
  "name": "extract-sharepoint-links-in-mail",
@@ -203,7 +652,13 @@
203
652
  "name": "message-id",
204
653
  "key": "messageId",
205
654
  "required": true,
206
- "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
655
+ "aliases": [
656
+ {
657
+ "name": "id",
658
+ "key": "id"
659
+ }
660
+ ],
661
+ "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`. Accepts `--id` as an alias."
207
662
  }
208
663
  ],
209
664
  "example": "ask-marcel extract-sharepoint-links-in-mail --message-id 'AAMkADk0...'",
@@ -211,7 +666,7 @@
211
666
  },
212
667
  {
213
668
  "name": "find-chats-with-user",
214
- "summary": "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.",
669
+ "summary": "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.",
215
670
  "category": "chats",
216
671
  "graphMethod": "GET",
217
672
  "graphPathTemplate": "https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats",
@@ -221,7 +676,7 @@
221
676
  "name": "name",
222
677
  "key": "name",
223
678
  "required": true,
224
- "description": "Substring to search across each chat member's `displayName`, `email`, `userPrincipalName`, `givenName`, `surname`, `mri`, `objectId`, and `jobTitle`. Both the query and each field are NFD-normalized + diacritics-stripped + lowercased before comparison, so `Hervé` ↔ `Herve` ↔ `HERVÉ` are equivalent and a query for the accented name still matches a member whose displayName is the un-accented email. Use the full name or an unambiguous fragment. Quoted multi-word values match on the joined substring, not per-token."
679
+ "description": "Substring to search across each chat member's `displayName`, `email`, `userPrincipalName`, `givenName`, `surname`, `mri`, `objectId`, and `jobTitle`. Both the query and each field are NFD-normalized + diacritics-stripped + lowercased before comparison, so `Jane` ↔ `Jane` ↔ `JANE` are equivalent and a query for the accented name still matches a member whose displayName is the un-accented email. Use the full name or an unambiguous fragment. Quoted multi-word values match on the joined substring, not per-token."
225
680
  },
226
681
  {
227
682
  "name": "max-pages",
@@ -236,8 +691,9 @@
236
691
  "description": "Chats per page (positive integer; default 100, same value Teams web uses). Server may silently cap."
237
692
  }
238
693
  ],
239
- "example": "ask-marcel find-chats-with-user --name 'Hervé ATTANE'",
694
+ "example": "ask-marcel find-chats-with-user --name 'Jane DOE'",
240
695
  "responseShape": "`{ name, matches: [{ chatId, title, chatType, threadType, memberCount, lastMessageAt?, matchedMembers: [{ mri, displayName, email, userSubType }] }], matchCount, pagesFetched, chatsScanned, hasMore, nextContinuationToken? }`. `matchedMembers` always carries the matching entries' identifying fields — pass `chatId` into `list-teams-chat-history` to read message bodies. `hasMore: true` means `--max-pages` was hit before exhausting the chat list; chain with the existing `--continuation-token` flag on `list-teams-chats-with-messages` if you need to scan further (this command does not advertise a `--continuation-token` because resuming a partial search is rare; users either widen `--max-pages` or refine `--name`).",
696
+ "needsSubstrateToken": true,
241
697
  "stability": "experimental"
242
698
  },
243
699
  {
@@ -252,6 +708,12 @@
252
708
  "name": "event-id",
253
709
  "key": "eventId",
254
710
  "required": true,
711
+ "aliases": [
712
+ {
713
+ "name": "id",
714
+ "key": "id"
715
+ }
716
+ ],
255
717
  "description": "Microsoft Graph event ID. Returned by `ask-marcel list-calendar-events` in the `id` field of each event."
256
718
  },
257
719
  {
@@ -272,7 +734,7 @@
272
734
  },
273
735
  {
274
736
  "name": "get-channel-files-folder",
275
- "summary": "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`.",
737
+ "summary": "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`.",
276
738
  "category": "teams",
277
739
  "graphMethod": "GET",
278
740
  "graphPathTemplate": "/teams/{team-id}/channels/{channel-id}/filesFolder",
@@ -318,6 +780,12 @@
318
780
  "name": "chat-id",
319
781
  "key": "chatId",
320
782
  "required": true,
783
+ "aliases": [
784
+ {
785
+ "name": "id",
786
+ "key": "id"
787
+ }
788
+ ],
321
789
  "description": "Microsoft Teams chat ID, e.g. `19:abc...@thread.v2`. Returned by `list-chats`."
322
790
  },
323
791
  {
@@ -334,7 +802,8 @@
334
802
  }
335
803
  ],
336
804
  "example": "ask-marcel get-chat --chat-id '19:abc...@thread.v2'",
337
- "responseShape": "single Microsoft Graph `chat` resource projected to the default `--select` set (or, when overridden, to the requested fields). `--expand members` adds an inline `members[]` array."
805
+ "responseShape": "single Microsoft Graph `chat` resource projected to the default `--select` set (or, when overridden, to the requested fields). `--expand members` adds an inline `members[]` array.",
806
+ "needsElevatedToken": true
338
807
  },
339
808
  {
340
809
  "name": "get-current-user",
@@ -372,7 +841,7 @@
372
841
  "name": "drive-id",
373
842
  "key": "driveId",
374
843
  "required": true,
375
- "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
844
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
376
845
  },
377
846
  {
378
847
  "name": "item-id",
@@ -433,7 +902,7 @@
433
902
  "name": "drive-id",
434
903
  "key": "driveId",
435
904
  "required": true,
436
- "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
905
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
437
906
  },
438
907
  {
439
908
  "name": "item-id",
@@ -469,7 +938,7 @@
469
938
  "name": "drive-id",
470
939
  "key": "driveId",
471
940
  "required": true,
472
- "description": "OneDrive / SharePoint drive ID."
941
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
473
942
  },
474
943
  {
475
944
  "name": "item-id",
@@ -493,7 +962,7 @@
493
962
  "name": "drive-id",
494
963
  "key": "driveId",
495
964
  "required": true,
496
- "description": "OneDrive / SharePoint drive ID."
965
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
497
966
  },
498
967
  {
499
968
  "name": "item-id",
@@ -529,7 +998,7 @@
529
998
  "name": "drive-id",
530
999
  "key": "driveId",
531
1000
  "required": true,
532
- "description": "OneDrive / SharePoint drive ID."
1001
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
533
1002
  },
534
1003
  {
535
1004
  "name": "item-id",
@@ -565,7 +1034,7 @@
565
1034
  "name": "drive-id",
566
1035
  "key": "driveId",
567
1036
  "required": true,
568
- "description": "OneDrive / SharePoint drive ID."
1037
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
569
1038
  },
570
1039
  {
571
1040
  "name": "item-id",
@@ -644,7 +1113,13 @@
644
1113
  "name": "drive-id",
645
1114
  "key": "driveId",
646
1115
  "required": true,
647
- "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives` in the `id` field."
1116
+ "aliases": [
1117
+ {
1118
+ "name": "id",
1119
+ "key": "id"
1120
+ }
1121
+ ],
1122
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
648
1123
  },
649
1124
  {
650
1125
  "name": "select",
@@ -664,7 +1139,7 @@
664
1139
  },
665
1140
  {
666
1141
  "name": "get-drive-special-folder",
667
- "summary": "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`.",
1142
+ "summary": "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`.",
668
1143
  "category": "drive",
669
1144
  "graphMethod": "GET",
670
1145
  "graphPathTemplate": "/me/drive/special/{folder-name}",
@@ -692,6 +1167,49 @@
692
1167
  "example": "ask-marcel get-drive-special-folder --folder-name 'documents'",
693
1168
  "responseShape": "single Microsoft Graph `driveItem` resource (folder)"
694
1169
  },
1170
+ {
1171
+ "name": "get-excel-chart-image",
1172
+ "summary": "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`.",
1173
+ "category": "excel",
1174
+ "graphMethod": "GET",
1175
+ "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/charts/{chart-id}/Image(width=0,height=0,fittingMode='Fit')",
1176
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chart-image",
1177
+ "options": [
1178
+ {
1179
+ "name": "drive-id",
1180
+ "key": "driveId",
1181
+ "required": true,
1182
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
1183
+ },
1184
+ {
1185
+ "name": "item-id",
1186
+ "key": "itemId",
1187
+ "required": true,
1188
+ "description": "driveItem ID of the .xlsx file."
1189
+ },
1190
+ {
1191
+ "name": "worksheet-id",
1192
+ "key": "worksheetId",
1193
+ "required": true,
1194
+ "description": "Worksheet display name (e.g. `Sheet1`) or the worksheet `id` GUID returned by `list-excel-worksheets`.",
1195
+ "argumentHint": {
1196
+ "kind": "idOrName"
1197
+ }
1198
+ },
1199
+ {
1200
+ "name": "chart-id",
1201
+ "key": "chartId",
1202
+ "required": true,
1203
+ "description": "Chart name (e.g. `Chart 1`) or `id`, as returned by `list-excel-worksheet-charts`.",
1204
+ "argumentHint": {
1205
+ "kind": "idOrName"
1206
+ }
1207
+ }
1208
+ ],
1209
+ "example": "ask-marcel get-excel-chart-image --drive-id 'b!1234' --item-id '01ABC' --worksheet-id 'Sheet1' --chart-id 'Chart 1' --output-path ./chart.png",
1210
+ "responseShape": "`{ contentType: \"image/png\", size, base64 }` — the rendered chart PNG, inlined. Pair with the global `--output-path <file>` to write the PNG to disk (the response then replaces `base64` with `savedTo`).",
1211
+ "producesBytes": true
1212
+ },
695
1213
  {
696
1214
  "name": "get-excel-range",
697
1215
  "summary": "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.",
@@ -704,7 +1222,7 @@
704
1222
  "name": "drive-id",
705
1223
  "key": "driveId",
706
1224
  "required": true,
707
- "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
1225
+ "description": "Microsoft Graph drive ID containing the workbook. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
708
1226
  },
709
1227
  {
710
1228
  "name": "item-id",
@@ -746,7 +1264,7 @@
746
1264
  "name": "drive-id",
747
1265
  "key": "driveId",
748
1266
  "required": true,
749
- "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
1267
+ "description": "Microsoft Graph drive ID containing the workbook. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
750
1268
  },
751
1269
  {
752
1270
  "name": "item-id",
@@ -776,7 +1294,7 @@
776
1294
  "name": "drive-id",
777
1295
  "key": "driveId",
778
1296
  "required": true,
779
- "description": "OneDrive / SharePoint drive ID."
1297
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
780
1298
  },
781
1299
  {
782
1300
  "name": "item-id",
@@ -821,6 +1339,12 @@
821
1339
  "name": "group-id",
822
1340
  "key": "groupId",
823
1341
  "required": true,
1342
+ "aliases": [
1343
+ {
1344
+ "name": "id",
1345
+ "key": "id"
1346
+ }
1347
+ ],
824
1348
  "description": "Azure AD group object ID. Use `list-groups` to find one."
825
1349
  },
826
1350
  {
@@ -841,7 +1365,7 @@
841
1365
  },
842
1366
  {
843
1367
  "name": "get-mail-attachment",
844
- "summary": "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.",
1368
+ "summary": "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.",
845
1369
  "category": "mail",
846
1370
  "graphMethod": "GET",
847
1371
  "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
@@ -873,7 +1397,8 @@
873
1397
  }
874
1398
  ],
875
1399
  "example": "ask-marcel get-mail-attachment --message-id 'AAMkAGI2...' --attachment-id 'AAMkABC...'",
876
- "responseShape": "single Microsoft Graph `attachment` resource. fileAttachments include `contentBytes` (Graph) AND `base64` (CLI mirror) so `--output-path` works; itemAttachments and referenceAttachments are returned unchanged."
1400
+ "responseShape": "single Microsoft Graph `attachment` resource. fileAttachments include `contentBytes` (Graph) AND `base64` (CLI mirror) so `--output-path` works; with `--output-path` set, both byte fields are stripped from stdout and replaced by `savedTo`. itemAttachments and referenceAttachments are returned unchanged.",
1401
+ "producesBytes": true
877
1402
  },
878
1403
  {
879
1404
  "name": "get-mail-message",
@@ -887,7 +1412,13 @@
887
1412
  "name": "message-id",
888
1413
  "key": "messageId",
889
1414
  "required": true,
890
- "description": "Outlook message ID. Returned by `ask-marcel list-mail-messages` or `list-mail-folder-messages`."
1415
+ "aliases": [
1416
+ {
1417
+ "name": "id",
1418
+ "key": "id"
1419
+ }
1420
+ ],
1421
+ "description": "Outlook message ID. Returned by `ask-marcel list-mail-messages` or `list-mail-folder-messages`. Accepts `--id` as an alias."
891
1422
  },
892
1423
  {
893
1424
  "name": "select",
@@ -917,11 +1448,18 @@
917
1448
  "name": "message-id",
918
1449
  "key": "messageId",
919
1450
  "required": true,
920
- "description": "Outlook message ID. Returned by `list-mail-messages` or `search-mail-messages`."
1451
+ "aliases": [
1452
+ {
1453
+ "name": "id",
1454
+ "key": "id"
1455
+ }
1456
+ ],
1457
+ "description": "Outlook message ID. Returned by `list-mail-messages` or `search-mail-messages`. Accepts `--id` as an alias."
921
1458
  }
922
1459
  ],
923
1460
  "example": "ask-marcel get-mail-message-mime --message-id 'AAMkAD...'",
924
- "responseShape": "`{ contentType: \"text/plain\", size, base64 }` — Graph returns the raw MIME envelope wrapped as `text/plain` (NOT `message/rfc822` as the older docs suggested). Pair with the global `--output-path <path>` flag to land the .eml on disk and replace `base64` with `savedTo` for messages with large attachments."
1461
+ "responseShape": "`{ contentType: \"text/plain\", size, base64 }` — Graph returns the raw MIME envelope wrapped as `text/plain` (NOT `message/rfc822` as the older docs suggested). Pair with the global `--output-path <path>` flag to land the .eml on disk and replace `base64` with `savedTo` for messages with large attachments.",
1462
+ "producesBytes": true
925
1463
  },
926
1464
  {
927
1465
  "name": "get-mail-rule",
@@ -943,6 +1481,10 @@
943
1481
  "required": true,
944
1482
  "description": "Message rule ID. Returned by `list-mail-rules`.",
945
1483
  "aliases": [
1484
+ {
1485
+ "name": "id",
1486
+ "key": "id"
1487
+ },
946
1488
  {
947
1489
  "name": "rule-id",
948
1490
  "key": "ruleId"
@@ -1021,11 +1563,12 @@
1021
1563
  "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/profilephoto-get",
1022
1564
  "options": [],
1023
1565
  "example": "ask-marcel get-my-profile-photo",
1024
- "responseShape": "`{ contentType: \"image/jpeg\", size: <bytes>, base64: \"<encoded>\" }` — the photo bytes, inlined. Pair with the global `--output-path <path>` flag to land the image on disk and replace `base64` with `savedTo`."
1566
+ "responseShape": "`{ contentType: \"image/jpeg\", size: <bytes>, base64: \"<encoded>\" }` — the photo bytes, inlined. Pair with the global `--output-path <path>` flag to land the image on disk and replace `base64` with `savedTo`.",
1567
+ "producesBytes": true
1025
1568
  },
1026
1569
  {
1027
1570
  "name": "get-onenote-page-as-markdown",
1028
- "summary": "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`.",
1571
+ "summary": "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`.",
1029
1572
  "category": "notes",
1030
1573
  "graphMethod": "GET",
1031
1574
  "graphPathTemplate": "/me/onenote/pages/{onenote-page-id}/content",
@@ -1037,15 +1580,46 @@
1037
1580
  "required": true,
1038
1581
  "description": "OneNote page ID. Returned by `ask-marcel list-onenote-section-pages`.",
1039
1582
  "aliases": [
1583
+ {
1584
+ "name": "id",
1585
+ "key": "id"
1586
+ },
1040
1587
  {
1041
1588
  "name": "page-id",
1042
1589
  "key": "pageId"
1043
1590
  }
1044
1591
  ]
1592
+ },
1593
+ {
1594
+ "name": "inline-images",
1595
+ "key": "inlineImages",
1596
+ "required": false,
1597
+ "description": "Pass `--inline-images false` to skip fetching + embedding the page’s `onenote/resources/{id}/$value` images and keep the raw Graph resource URLs in the markdown. Default is `true` (embed as base64 `data:` URIs so the output is self-contained). Embedding is per-image isolated — a failed / oversize / non-image resource is left as a URL either way.",
1598
+ "argumentHint": {
1599
+ "kind": "magicValue",
1600
+ "values": [
1601
+ "true",
1602
+ "false"
1603
+ ]
1604
+ }
1605
+ },
1606
+ {
1607
+ "name": "include-metadata",
1608
+ "key": "includeMetadata",
1609
+ "required": false,
1610
+ "description": "Pass `--include-metadata true` to append a `## OneNote metadata` block after the body: page title, created / last-modified timestamps, and the parent section + notebook display names (one extra GET, expanded). Default omits it.",
1611
+ "argumentHint": {
1612
+ "kind": "magicValue",
1613
+ "values": [
1614
+ "true",
1615
+ "false"
1616
+ ]
1617
+ }
1045
1618
  }
1046
1619
  ],
1047
1620
  "example": "ask-marcel get-onenote-page-as-markdown --onenote-page-id '1-abc...'",
1048
- "responseShape": "`{ contentType: \"text/markdown\", size: <chars>, text: \"...\" }` — turndown-rendered markdown of the OneNote page body."
1621
+ "responseShape": "`{ contentType: \"text/markdown\", size, text }` — turndown-rendered page body with inline images embedded as data URIs by default. With `--include-metadata true`, a `## OneNote metadata` block is appended after the body.",
1622
+ "producesBytes": true
1049
1623
  },
1050
1624
  {
1051
1625
  "name": "get-onenote-page-content",
@@ -1061,6 +1635,10 @@
1061
1635
  "required": true,
1062
1636
  "description": "OneNote page ID. Returned by `ask-marcel list-onenote-section-pages`.",
1063
1637
  "aliases": [
1638
+ {
1639
+ "name": "id",
1640
+ "key": "id"
1641
+ },
1064
1642
  {
1065
1643
  "name": "page-id",
1066
1644
  "key": "pageId"
@@ -1069,7 +1647,8 @@
1069
1647
  }
1070
1648
  ],
1071
1649
  "example": "ask-marcel get-onenote-page-content --onenote-page-id '1-abc...'",
1072
- "responseShape": "`{ contentType: \"text/html\", size: <chars>, text: \"<html>...\" }` — the rendered OneNote page body wrapped in a JSON envelope"
1650
+ "responseShape": "`{ contentType: \"text/html\", size: <chars>, text: \"<html>...\" }` — the rendered OneNote page body wrapped in a JSON envelope",
1651
+ "producesBytes": true
1073
1652
  },
1074
1653
  {
1075
1654
  "name": "get-organization",
@@ -1109,6 +1688,10 @@
1109
1688
  "required": true,
1110
1689
  "description": "Planner bucket ID. Returned by `ask-marcel list-plan-buckets`.",
1111
1690
  "aliases": [
1691
+ {
1692
+ "name": "id",
1693
+ "key": "id"
1694
+ },
1112
1695
  {
1113
1696
  "name": "bucket-id",
1114
1697
  "key": "bucketId"
@@ -1133,6 +1716,10 @@
1133
1716
  "required": true,
1134
1717
  "description": "Planner plan ID. Returned in the `planId` field of any task from `ask-marcel list-planner-tasks`.",
1135
1718
  "aliases": [
1719
+ {
1720
+ "name": "id",
1721
+ "key": "id"
1722
+ },
1136
1723
  {
1137
1724
  "name": "plan-id",
1138
1725
  "key": "planId"
@@ -1157,6 +1744,10 @@
1157
1744
  "required": true,
1158
1745
  "description": "Planner task ID. Returned by `ask-marcel list-planner-tasks` or `list-plan-tasks`. Accepts `--task-id` as a shorter alias (each task command targets exactly one of Planner or To Do, so within this command's flag set there is no ambiguity).",
1159
1746
  "aliases": [
1747
+ {
1748
+ "name": "id",
1749
+ "key": "id"
1750
+ },
1160
1751
  {
1161
1752
  "name": "task-id",
1162
1753
  "key": "taskId"
@@ -1181,6 +1772,10 @@
1181
1772
  "required": true,
1182
1773
  "description": "Planner task ID. Returned by `ask-marcel list-planner-tasks` or `list-plan-tasks`. Accepts `--task-id` as a shorter alias (each task command targets exactly one of Planner or To Do, so within this command's flag set there is no ambiguity).",
1183
1774
  "aliases": [
1775
+ {
1776
+ "name": "id",
1777
+ "key": "id"
1778
+ },
1184
1779
  {
1185
1780
  "name": "task-id",
1186
1781
  "key": "taskId"
@@ -1284,6 +1879,12 @@
1284
1879
  "name": "site-id",
1285
1880
  "key": "siteId",
1286
1881
  "required": true,
1882
+ "aliases": [
1883
+ {
1884
+ "name": "id",
1885
+ "key": "id"
1886
+ }
1887
+ ],
1287
1888
  "description": "SharePoint site ID. Either the composite ID (`hostname,site-collection-id,site-id`) returned by `ask-marcel search-sharepoint-sites-by-name`, or the literal `root` to refer to the tenant root site."
1288
1889
  },
1289
1890
  {
@@ -1480,7 +2081,8 @@
1480
2081
  }
1481
2082
  ],
1482
2083
  "example": "ask-marcel get-sharepoint-site-onenote-page-content --site-id 'contoso.sharepoint.com,...' --onenote-page-id 'p1'",
1483
- "responseShape": "`{ contentType: \"text/html\", size: <chars>, text: \"<html>...\" }` — the rendered OneNote page body wrapped in a JSON envelope. Pair with the global `--output-path <path>` to write the raw HTML to disk."
2084
+ "responseShape": "`{ contentType: \"text/html\", size: <chars>, text: \"<html>...\" }` — the rendered OneNote page body wrapped in a JSON envelope. Pair with the global `--output-path <path>` to write the raw HTML to disk.",
2085
+ "producesBytes": true
1484
2086
  },
1485
2087
  {
1486
2088
  "name": "get-site-analytics",
@@ -1494,6 +2096,12 @@
1494
2096
  "name": "site-id",
1495
2097
  "key": "siteId",
1496
2098
  "required": true,
2099
+ "aliases": [
2100
+ {
2101
+ "name": "id",
2102
+ "key": "id"
2103
+ }
2104
+ ],
1497
2105
  "description": "SharePoint site ID."
1498
2106
  }
1499
2107
  ],
@@ -1548,6 +2156,12 @@
1548
2156
  "name": "team-id",
1549
2157
  "key": "teamId",
1550
2158
  "required": true,
2159
+ "aliases": [
2160
+ {
2161
+ "name": "id",
2162
+ "key": "id"
2163
+ }
2164
+ ],
1551
2165
  "description": "Microsoft Teams team ID. Returned by `ask-marcel list-joined-teams`."
1552
2166
  },
1553
2167
  {
@@ -1614,6 +2228,12 @@
1614
2228
  "name": "team-id",
1615
2229
  "key": "teamId",
1616
2230
  "required": true,
2231
+ "aliases": [
2232
+ {
2233
+ "name": "id",
2234
+ "key": "id"
2235
+ }
2236
+ ],
1617
2237
  "description": "Microsoft Teams team ID. Returned by `list-joined-teams`."
1618
2238
  },
1619
2239
  {
@@ -1655,11 +2275,12 @@
1655
2275
  ],
1656
2276
  "example": "ask-marcel get-teams-chat-message --chat-id '19:abc...@unq.gbl.spaces' --message-id '1700000000000'",
1657
2277
  "responseShape": "single Teams chat message — `id`, `from`, `imDisplayName`, `content`, `contentType`, `composeTime`, `originalArrivalTime`, etc. **Microsoft-internal schema — fields may change without notice.**",
2278
+ "needsSubstrateToken": true,
1658
2279
  "stability": "experimental"
1659
2280
  },
1660
2281
  {
1661
2282
  "name": "get-todo-task",
1662
- "summary": "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.",
2283
+ "summary": "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.",
1663
2284
  "category": "tasks",
1664
2285
  "graphMethod": "GET",
1665
2286
  "graphPathTemplate": "/me/todo/lists/{todo-task-list-id}/tasks/{todo-task-id}",
@@ -1721,6 +2342,12 @@
1721
2342
  "name": "user-id",
1722
2343
  "key": "userId",
1723
2344
  "required": true,
2345
+ "aliases": [
2346
+ {
2347
+ "name": "id",
2348
+ "key": "id"
2349
+ }
2350
+ ],
1724
2351
  "description": "Azure AD user ID or UPN — typically the user's email address. Discover via `list-relevant-people` (relevance-ranked colleagues) or `microsoft-search-query --query <name>` (federated person search across the tenant directory)."
1725
2352
  },
1726
2353
  {
@@ -1739,6 +2366,36 @@
1739
2366
  "example": "ask-marcel get-user-manager --user-id 'alice@contoso.com' --select 'id,displayName,mail'",
1740
2367
  "responseShape": "single Microsoft Graph `user` resource on success, OR `{ manager: null, note: <string> }` when the target user has no manager set. Detect the no-manager case via `data.manager === null` (same discriminator as `get-my-manager`)."
1741
2368
  },
2369
+ {
2370
+ "name": "list-accessible-drives",
2371
+ "summary": "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.",
2372
+ "category": "drive",
2373
+ "graphMethod": "GET",
2374
+ "graphPathTemplate": "/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",
2375
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-list",
2376
+ "options": [
2377
+ {
2378
+ "name": "max-groups",
2379
+ "key": "maxGroups",
2380
+ "required": false,
2381
+ "description": "Safety cap on each fan-out (positive integer; default 100): the per-group `/groups/{id}/drive` calls, the per-team `/teams/{id}/channels` enumeration, the per-private/shared-channel `filesFolder` lookups, the per-site `/sites/{id}/drives` enumeration, and the `/drives/{id}` enrichment for shared-, channel-, and activity-only drives. A user in hundreds of teams/groups/sites would otherwise issue hundreds of parallel requests (429 risk). When any cap is hit the response carries `truncated: true`.",
2382
+ "argumentHint": {
2383
+ "kind": "magicValue",
2384
+ "values": [
2385
+ "100"
2386
+ ]
2387
+ }
2388
+ },
2389
+ {
2390
+ "name": "count-files",
2391
+ "key": "countFiles",
2392
+ "required": false,
2393
+ "description": "Pass `--count-files true` to add `estimatedFileCount` to each drive — the Microsoft Search index's security-trimmed `driveItem` total (files + folders) scoped to that drive's `webUrl` via KQL `path:`. OFF by default because it issues ONE extra Search query per drive (chunked, capped at 200) — a real fan-out with 429-throttling risk on large drive sets. It is an estimate, not an exact count."
2394
+ }
2395
+ ],
2396
+ "example": "ask-marcel list-accessible-drives --output json",
2397
+ "responseShape": "`{ value: [{ id, name, driveType, webUrl, sources: [\"activity\"|\"channel\"|\"joinedTeam\"|\"memberOfGroup\"|\"personal\"|\"sharedWithMe\"|\"siteLibrary\"], groupId?, size?, estimatedFileCount? }], count, fileEstimate?, truncated?: true, partialErrors?: [{ source, error }] }`. `estimatedFileCount` appears only with `--count-files true` — the security-trimmed `driveItem` (files+folders) estimate scoped to each drive, omitted past the 200-drive count cap or when the per-drive query fails. `value[]` is deduped by drive `id` and sorted by id; `sources[]` lists every vector that surfaced the drive (`channel` = a private/shared Teams channel files folder; `activity` = a recently-used / followed / trending item drive; `siteLibrary` = a non-default document library of a discovered site); `groupId` is present only for Teams/group drives. `size` (when present) is the drive's total bytes used (`quota.used`, recursive) — surfaced free from the drive resource, omitted for the rare drive without a quota facet; it is a data-volume signal, not a file count. `fileEstimate` (best-effort, omitted if the extra query fails) is the Microsoft Search index's security-trimmed `driveItem` count — roughly how many files+folders you can access across ALL of SharePoint/OneDrive; it is INDEX-WIDE, not limited to the `value[]` drives above. `truncated: true` means a `--max-groups` cap was hit — raise it to see more. `partialErrors[]` (present only when something actionable failed) names each vector/group/channel/site whose sub-call returned an actionable error (auth, throttling, 5xx, network); benign \"can't reach this one\" results (404/403/423/400) are dropped, not listed."
2398
+ },
1742
2399
  {
1743
2400
  "name": "list-all-onenote-sections",
1744
2401
  "summary": "List every OneNote section the signed-in user can see, across all notebooks.",
@@ -1784,8 +2441,69 @@
1784
2441
  "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
1785
2442
  }
1786
2443
  ],
1787
- "example": "ask-marcel list-all-onenote-sections",
1788
- "responseShape": "collection of Microsoft Graph `onenoteSection` resources under `value[]`",
2444
+ "example": "ask-marcel list-all-onenote-sections",
2445
+ "responseShape": "collection of Microsoft Graph `onenoteSection` resources under `value[]`",
2446
+ "pagination": true
2447
+ },
2448
+ {
2449
+ "name": "list-calendar-event-attachments",
2450
+ "summary": "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).",
2451
+ "category": "calendar",
2452
+ "graphMethod": "GET",
2453
+ "graphPathTemplate": "/me/events/{event-id}/attachments",
2454
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/event-list-attachments",
2455
+ "options": [
2456
+ {
2457
+ "name": "event-id",
2458
+ "key": "eventId",
2459
+ "required": true,
2460
+ "aliases": [
2461
+ {
2462
+ "name": "id",
2463
+ "key": "id"
2464
+ }
2465
+ ],
2466
+ "description": "Outlook calendar event ID. Returned by `ask-marcel list-calendar-events` or `get-calendar-event`."
2467
+ },
2468
+ {
2469
+ "name": "top",
2470
+ "key": "top",
2471
+ "required": false,
2472
+ "description": "OData $top: maximum number of items to return on this page (positive integer, ≤ 1000). Graph silently caps at 1000 on every collection endpoint, so the CLI rejects larger values with a clear validation error rather than letting the request silently truncate. Combine with `next-page` to paginate beyond the cap."
2473
+ },
2474
+ {
2475
+ "name": "skip",
2476
+ "key": "skip",
2477
+ "required": false,
2478
+ "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
2479
+ },
2480
+ {
2481
+ "name": "select",
2482
+ "key": "select",
2483
+ "required": false,
2484
+ "description": "OData $select: comma-separated list of fields to include in each item (e.g. `id,subject,from`). May shrink payloads dramatically — Graph honors $select on most endpoints, but some collections (notably `/me/mailboxSettings`, `/me/outlook/masterCategories`, `/me/mailFolders/inbox/messageRules`) silently ignore it and always return the full resource. Bogus field names are silently dropped by Graph — if a field you asked for is missing from the response, double-check the spelling against the documented `responseShape`."
2485
+ },
2486
+ {
2487
+ "name": "filter",
2488
+ "key": "filter",
2489
+ "required": false,
2490
+ "description": "OData $filter: predicate to narrow results server-side. Quoting rules: string literals MUST use SINGLE quotes (`subject eq 'invoice'`), NOT double quotes — Graph rejects `subject eq \"invoice\"` with `InvalidFilterClause`. To embed a single quote inside a string, double it (`subject eq 'O''Brien'`). Booleans, numbers, and dates are unquoted (`isRead eq false`, `receivedDateTime ge 2026-01-01T00:00:00Z`). Wrap the whole flag value in shell DOUBLE quotes so the inner single quotes survive (`--filter \"subject eq 'invoice'\"`). Same syntax Graph documents per resource type."
2491
+ },
2492
+ {
2493
+ "name": "orderby",
2494
+ "key": "orderby",
2495
+ "required": false,
2496
+ "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
2497
+ },
2498
+ {
2499
+ "name": "expand",
2500
+ "key": "expand",
2501
+ "required": false,
2502
+ "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
2503
+ }
2504
+ ],
2505
+ "example": "ask-marcel list-calendar-event-attachments --event-id 'AAMkAGI2...'",
2506
+ "responseShape": "collection of Microsoft Graph `attachment` resources under `value[]` (slim metadata by default — see summary). Graph always includes `@odata.type` and `@odata.mediaContentType` on every entry regardless of `--select` — that discriminator is what the attachment-converting commands branch on.",
1789
2507
  "pagination": true
1790
2508
  },
1791
2509
  {
@@ -1813,18 +2531,36 @@
1813
2531
  "name": "event-id",
1814
2532
  "key": "eventId",
1815
2533
  "required": true,
2534
+ "aliases": [
2535
+ {
2536
+ "name": "id",
2537
+ "key": "id"
2538
+ }
2539
+ ],
1816
2540
  "description": "Recurring event ID. Returned by `ask-marcel list-specific-calendar-events`."
1817
2541
  },
1818
2542
  {
1819
2543
  "name": "start-date-time",
1820
2544
  "key": "startDateTime",
1821
2545
  "required": true,
2546
+ "aliases": [
2547
+ {
2548
+ "name": "start",
2549
+ "key": "start"
2550
+ }
2551
+ ],
1822
2552
  "description": "Lower bound. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
1823
2553
  },
1824
2554
  {
1825
2555
  "name": "end-date-time",
1826
2556
  "key": "endDateTime",
1827
2557
  "required": true,
2558
+ "aliases": [
2559
+ {
2560
+ "name": "end",
2561
+ "key": "end"
2562
+ }
2563
+ ],
1828
2564
  "description": "Upper bound. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
1829
2565
  },
1830
2566
  {
@@ -1948,6 +2684,12 @@
1948
2684
  "name": "calendar-group-id",
1949
2685
  "key": "calendarGroupId",
1950
2686
  "required": true,
2687
+ "aliases": [
2688
+ {
2689
+ "name": "id",
2690
+ "key": "id"
2691
+ }
2692
+ ],
1951
2693
  "description": "Calendar group ID. Returned by `list-calendar-groups`."
1952
2694
  },
1953
2695
  {
@@ -2052,12 +2794,24 @@
2052
2794
  "name": "start-date-time",
2053
2795
  "key": "startDateTime",
2054
2796
  "required": true,
2797
+ "aliases": [
2798
+ {
2799
+ "name": "start",
2800
+ "key": "start"
2801
+ }
2802
+ ],
2055
2803
  "description": "Lower bound. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
2056
2804
  },
2057
2805
  {
2058
2806
  "name": "end-date-time",
2059
2807
  "key": "endDateTime",
2060
2808
  "required": true,
2809
+ "aliases": [
2810
+ {
2811
+ "name": "end",
2812
+ "key": "end"
2813
+ }
2814
+ ],
2061
2815
  "description": "Upper bound. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
2062
2816
  },
2063
2817
  {
@@ -2112,12 +2866,24 @@
2112
2866
  "name": "start-date-time",
2113
2867
  "key": "startDateTime",
2114
2868
  "required": true,
2869
+ "aliases": [
2870
+ {
2871
+ "name": "start",
2872
+ "key": "start"
2873
+ }
2874
+ ],
2115
2875
  "description": "Lower bound (required on the first call only — the deltaLink token encodes it for resumes). ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
2116
2876
  },
2117
2877
  {
2118
2878
  "name": "end-date-time",
2119
2879
  "key": "endDateTime",
2120
2880
  "required": true,
2881
+ "aliases": [
2882
+ {
2883
+ "name": "end",
2884
+ "key": "end"
2885
+ }
2886
+ ],
2121
2887
  "description": "Upper bound (required on the first call only — the deltaLink token encodes it for resumes). ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
2122
2888
  },
2123
2889
  {
@@ -2192,6 +2958,12 @@
2192
2958
  "name": "chat-id",
2193
2959
  "key": "chatId",
2194
2960
  "required": true,
2961
+ "aliases": [
2962
+ {
2963
+ "name": "id",
2964
+ "key": "id"
2965
+ }
2966
+ ],
2195
2967
  "description": "Microsoft Teams chat ID, e.g. `19:abc...@thread.v2`. Source the ID via `ask-marcel list-chats` (returns chat metadata for the signed-in user). Alternative sources outside the CLI: the Teams desktop / web client (Open in browser → URL contains the chat thread ID), Microsoft Graph Explorer, or URL-decode the `19%3ameeting_...%40thread.v2` segment of an `onlineMeeting.joinUrl` from `list-calendar-events`."
2196
2968
  },
2197
2969
  {
@@ -2215,7 +2987,8 @@
2215
2987
  ],
2216
2988
  "example": "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
2217
2989
  "responseShape": "collection of Microsoft Graph `conversationMember` resources under `value[]`",
2218
- "pagination": true
2990
+ "pagination": true,
2991
+ "needsElevatedToken": true
2219
2992
  },
2220
2993
  {
2221
2994
  "name": "list-chats",
@@ -2252,7 +3025,8 @@
2252
3025
  ],
2253
3026
  "example": "ask-marcel list-chats",
2254
3027
  "responseShape": "collection of Microsoft Graph `chat` resources under `value[]`, each projected to the default `--select` set (or, when overridden, to the requested fields).",
2255
- "pagination": true
3028
+ "pagination": true,
3029
+ "needsElevatedToken": true
2256
3030
  },
2257
3031
  {
2258
3032
  "name": "list-conversation-messages",
@@ -2266,6 +3040,12 @@
2266
3040
  "name": "conversation-id",
2267
3041
  "key": "conversationId",
2268
3042
  "required": true,
3043
+ "aliases": [
3044
+ {
3045
+ "name": "id",
3046
+ "key": "id"
3047
+ }
3048
+ ],
2269
3049
  "description": "Outlook `conversationId` of any message in the thread (returned by every mail-listing command and by `get-mail-message`)."
2270
3050
  },
2271
3051
  {
@@ -2309,7 +3089,7 @@
2309
3089
  "name": "drive-id",
2310
3090
  "key": "driveId",
2311
3091
  "required": true,
2312
- "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
3092
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2313
3093
  },
2314
3094
  {
2315
3095
  "name": "item-id",
@@ -2364,7 +3144,7 @@
2364
3144
  "name": "drive-id",
2365
3145
  "key": "driveId",
2366
3146
  "required": true,
2367
- "description": "OneDrive / SharePoint drive ID."
3147
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2368
3148
  },
2369
3149
  {
2370
3150
  "name": "item-id",
@@ -2523,7 +3303,7 @@
2523
3303
  "name": "drive-id",
2524
3304
  "key": "driveId",
2525
3305
  "required": true,
2526
- "description": "OneDrive / SharePoint drive ID."
3306
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2527
3307
  },
2528
3308
  {
2529
3309
  "name": "item-id",
@@ -2584,7 +3364,7 @@
2584
3364
  "name": "drive-id",
2585
3365
  "key": "driveId",
2586
3366
  "required": true,
2587
- "description": "OneDrive / SharePoint drive ID."
3367
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2588
3368
  },
2589
3369
  {
2590
3370
  "name": "item-id",
@@ -2645,7 +3425,7 @@
2645
3425
  "name": "drive-id",
2646
3426
  "key": "driveId",
2647
3427
  "required": true,
2648
- "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
3428
+ "description": "Microsoft Graph drive ID containing the workbook. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2649
3429
  },
2650
3430
  {
2651
3431
  "name": "item-id",
@@ -2700,7 +3480,7 @@
2700
3480
  "name": "drive-id",
2701
3481
  "key": "driveId",
2702
3482
  "required": true,
2703
- "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
3483
+ "description": "Microsoft Graph drive ID containing the workbook. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2704
3484
  },
2705
3485
  {
2706
3486
  "name": "item-id",
@@ -2749,7 +3529,7 @@
2749
3529
  "name": "drive-id",
2750
3530
  "key": "driveId",
2751
3531
  "required": true,
2752
- "description": "OneDrive / SharePoint drive ID."
3532
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2753
3533
  },
2754
3534
  {
2755
3535
  "name": "item-id",
@@ -2819,7 +3599,7 @@
2819
3599
  "name": "drive-id",
2820
3600
  "key": "driveId",
2821
3601
  "required": true,
2822
- "description": "OneDrive / SharePoint drive ID."
3602
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2823
3603
  },
2824
3604
  {
2825
3605
  "name": "item-id",
@@ -2889,7 +3669,7 @@
2889
3669
  "name": "drive-id",
2890
3670
  "key": "driveId",
2891
3671
  "required": true,
2892
- "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
3672
+ "description": "Microsoft Graph drive ID containing the workbook. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2893
3673
  },
2894
3674
  {
2895
3675
  "name": "item-id",
@@ -2981,7 +3761,7 @@
2981
3761
  "name": "drive-id",
2982
3762
  "key": "driveId",
2983
3763
  "required": true,
2984
- "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
3764
+ "description": "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
2985
3765
  },
2986
3766
  {
2987
3767
  "name": "item-id",
@@ -3085,18 +3865,36 @@
3085
3865
  "name": "group-id",
3086
3866
  "key": "groupId",
3087
3867
  "required": true,
3868
+ "aliases": [
3869
+ {
3870
+ "name": "id",
3871
+ "key": "id"
3872
+ }
3873
+ ],
3088
3874
  "description": "Azure AD group object ID for a unified (Microsoft 365) group."
3089
3875
  },
3090
3876
  {
3091
3877
  "name": "start-date-time",
3092
3878
  "key": "startDateTime",
3093
3879
  "required": true,
3880
+ "aliases": [
3881
+ {
3882
+ "name": "start",
3883
+ "key": "start"
3884
+ }
3885
+ ],
3094
3886
  "description": "Start of the window (recurrences are expanded across it). ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
3095
3887
  },
3096
3888
  {
3097
3889
  "name": "end-date-time",
3098
3890
  "key": "endDateTime",
3099
3891
  "required": true,
3892
+ "aliases": [
3893
+ {
3894
+ "name": "end",
3895
+ "key": "end"
3896
+ }
3897
+ ],
3100
3898
  "description": "End of the window. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
3101
3899
  },
3102
3900
  {
@@ -3152,6 +3950,12 @@
3152
3950
  "name": "group-id",
3153
3951
  "key": "groupId",
3154
3952
  "required": true,
3953
+ "aliases": [
3954
+ {
3955
+ "name": "id",
3956
+ "key": "id"
3957
+ }
3958
+ ],
3155
3959
  "description": "Azure AD group object ID for a unified (Microsoft 365) group."
3156
3960
  },
3157
3961
  {
@@ -3207,6 +4011,12 @@
3207
4011
  "name": "group-id",
3208
4012
  "key": "groupId",
3209
4013
  "required": true,
4014
+ "aliases": [
4015
+ {
4016
+ "name": "id",
4017
+ "key": "id"
4018
+ }
4019
+ ],
3210
4020
  "description": "Azure AD group object ID for a unified (Microsoft 365) group. Use `list-groups` to find one."
3211
4021
  },
3212
4022
  {
@@ -3262,6 +4072,12 @@
3262
4072
  "name": "group-id",
3263
4073
  "key": "groupId",
3264
4074
  "required": true,
4075
+ "aliases": [
4076
+ {
4077
+ "name": "id",
4078
+ "key": "id"
4079
+ }
4080
+ ],
3265
4081
  "description": "Azure AD group object ID. Use `list-groups` to find one."
3266
4082
  },
3267
4083
  {
@@ -3317,6 +4133,12 @@
3317
4133
  "name": "group-id",
3318
4134
  "key": "groupId",
3319
4135
  "required": true,
4136
+ "aliases": [
4137
+ {
4138
+ "name": "id",
4139
+ "key": "id"
4140
+ }
4141
+ ],
3320
4142
  "description": "Azure AD group object ID. Use `list-groups` to find one."
3321
4143
  },
3322
4144
  {
@@ -3372,6 +4194,12 @@
3372
4194
  "name": "group-id",
3373
4195
  "key": "groupId",
3374
4196
  "required": true,
4197
+ "aliases": [
4198
+ {
4199
+ "name": "id",
4200
+ "key": "id"
4201
+ }
4202
+ ],
3375
4203
  "description": "Azure AD group object ID for a unified (Microsoft 365) group."
3376
4204
  },
3377
4205
  {
@@ -3521,6 +4349,10 @@
3521
4349
  "required": true,
3522
4350
  "description": "todoTaskList ID. Returned by `ask-marcel list-todo-task-lists`. The well-known name `tasks` (the default list) is accepted on this incomplete-tasks endpoint specifically — sibling commands like `list-todo-tasks` and `list-todo-tasks-delta` only accept resolved IDs. There is no Graph endpoint that returns incomplete tasks across every list — call this once per list.",
3523
4351
  "aliases": [
4352
+ {
4353
+ "name": "id",
4354
+ "key": "id"
4355
+ },
3524
4356
  {
3525
4357
  "name": "task-list-id",
3526
4358
  "key": "taskListId"
@@ -3596,7 +4428,13 @@
3596
4428
  "name": "message-id",
3597
4429
  "key": "messageId",
3598
4430
  "required": true,
3599
- "description": "Outlook message ID. Returned by `ask-marcel list-mail-messages` or `list-mail-folder-messages`."
4431
+ "aliases": [
4432
+ {
4433
+ "name": "id",
4434
+ "key": "id"
4435
+ }
4436
+ ],
4437
+ "description": "Outlook message ID. Returned by `ask-marcel list-mail-messages` or `list-mail-folder-messages`. Accepts `--id` as an alias."
3600
4438
  },
3601
4439
  {
3602
4440
  "name": "top",
@@ -3651,6 +4489,12 @@
3651
4489
  "name": "mail-folder-id",
3652
4490
  "key": "mailFolderId",
3653
4491
  "required": true,
4492
+ "aliases": [
4493
+ {
4494
+ "name": "id",
4495
+ "key": "id"
4496
+ }
4497
+ ],
3654
4498
  "description": "mailFolder ID. Returned by `ask-marcel list-mail-folders`. Well-known names also work, e.g. `inbox`, `sentitems`, `drafts`."
3655
4499
  },
3656
4500
  {
@@ -3706,6 +4550,12 @@
3706
4550
  "name": "mail-folder-id",
3707
4551
  "key": "mailFolderId",
3708
4552
  "required": true,
4553
+ "aliases": [
4554
+ {
4555
+ "name": "id",
4556
+ "key": "id"
4557
+ }
4558
+ ],
3709
4559
  "description": "mailFolder ID. Returned by `ask-marcel list-mail-folders`. Well-known names also work, e.g. `inbox`, `sentitems`, `drafts`."
3710
4560
  },
3711
4561
  {
@@ -3761,6 +4611,12 @@
3761
4611
  "name": "mail-folder-id",
3762
4612
  "key": "mailFolderId",
3763
4613
  "required": true,
4614
+ "aliases": [
4615
+ {
4616
+ "name": "id",
4617
+ "key": "id"
4618
+ }
4619
+ ],
3764
4620
  "description": "Mail folder ID or well-known name (`inbox`, `archive`, `sentitems`, `deleteditems`, `junkemail`, `drafts`). Returned by `list-mail-folders`."
3765
4621
  },
3766
4622
  {
@@ -4091,6 +4947,12 @@
4091
4947
  "name": "notebook-id",
4092
4948
  "key": "notebookId",
4093
4949
  "required": true,
4950
+ "aliases": [
4951
+ {
4952
+ "name": "id",
4953
+ "key": "id"
4954
+ }
4955
+ ],
4094
4956
  "description": "OneNote notebook ID. Returned by `ask-marcel list-onenote-notebooks`."
4095
4957
  },
4096
4958
  {
@@ -4197,6 +5059,10 @@
4197
5059
  "required": true,
4198
5060
  "description": "OneNote section ID. Returned by `ask-marcel list-onenote-notebook-sections` or `list-all-onenote-sections`.",
4199
5061
  "aliases": [
5062
+ {
5063
+ "name": "id",
5064
+ "key": "id"
5065
+ },
4200
5066
  {
4201
5067
  "name": "section-id",
4202
5068
  "key": "sectionId"
@@ -4269,6 +5135,10 @@
4269
5135
  "required": true,
4270
5136
  "description": "Planner plan ID. Returned in the `planId` field of any task from `ask-marcel list-planner-tasks`.",
4271
5137
  "aliases": [
5138
+ {
5139
+ "name": "id",
5140
+ "key": "id"
5141
+ },
4272
5142
  {
4273
5143
  "name": "plan-id",
4274
5144
  "key": "planId"
@@ -4300,6 +5170,10 @@
4300
5170
  "required": true,
4301
5171
  "description": "Planner plan ID. Returned in the `planId` field of any task from `ask-marcel list-planner-tasks`.",
4302
5172
  "aliases": [
5173
+ {
5174
+ "name": "id",
5175
+ "key": "id"
5176
+ },
4303
5177
  {
4304
5178
  "name": "plan-id",
4305
5179
  "key": "planId"
@@ -4424,7 +5298,7 @@
4424
5298
  },
4425
5299
  {
4426
5300
  "name": "list-recently-used-insights",
4427
- "summary": "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`.",
5301
+ "summary": "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`.",
4428
5302
  "category": "drive",
4429
5303
  "graphMethod": "GET",
4430
5304
  "graphPathTemplate": "/me/insights/used",
@@ -4679,6 +5553,12 @@
4679
5553
  "name": "user-id",
4680
5554
  "key": "userId",
4681
5555
  "required": true,
5556
+ "aliases": [
5557
+ {
5558
+ "name": "id",
5559
+ "key": "id"
5560
+ }
5561
+ ],
4682
5562
  "description": "Azure AD user ID or UPN whose calendar to read. Requires `Calendars.Read.Shared` access (granted by the calendar owner)."
4683
5563
  },
4684
5564
  {
@@ -4734,18 +5614,36 @@
4734
5614
  "name": "user-id",
4735
5615
  "key": "userId",
4736
5616
  "required": true,
5617
+ "aliases": [
5618
+ {
5619
+ "name": "id",
5620
+ "key": "id"
5621
+ }
5622
+ ],
4737
5623
  "description": "Azure AD user ID or UPN of the calendar owner."
4738
5624
  },
4739
5625
  {
4740
5626
  "name": "start-date-time",
4741
5627
  "key": "startDateTime",
4742
5628
  "required": true,
5629
+ "aliases": [
5630
+ {
5631
+ "name": "start",
5632
+ "key": "start"
5633
+ }
5634
+ ],
4743
5635
  "description": "Start of the window. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
4744
5636
  },
4745
5637
  {
4746
5638
  "name": "end-date-time",
4747
5639
  "key": "endDateTime",
4748
5640
  "required": true,
5641
+ "aliases": [
5642
+ {
5643
+ "name": "end",
5644
+ "key": "end"
5645
+ }
5646
+ ],
4749
5647
  "description": "End of the window. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
4750
5648
  },
4751
5649
  {
@@ -4911,6 +5809,12 @@
4911
5809
  "name": "user-id",
4912
5810
  "key": "userId",
4913
5811
  "required": true,
5812
+ "aliases": [
5813
+ {
5814
+ "name": "id",
5815
+ "key": "id"
5816
+ }
5817
+ ],
4914
5818
  "description": "Azure AD user ID or UPN of the shared mailbox or delegated user. The signed-in user must have `Mail.Read.Shared` access (granted by the mailbox owner)."
4915
5819
  },
4916
5820
  {
@@ -4956,7 +5860,7 @@
4956
5860
  },
4957
5861
  {
4958
5862
  "name": "list-shared-with-me",
4959
- "summary": "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.",
5863
+ "summary": "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.",
4960
5864
  "category": "drive",
4961
5865
  "graphMethod": "GET",
4962
5866
  "graphPathTemplate": "/me/drive/sharedWithMe",
@@ -5080,6 +5984,12 @@
5080
5984
  "name": "site-id",
5081
5985
  "key": "siteId",
5082
5986
  "required": true,
5987
+ "aliases": [
5988
+ {
5989
+ "name": "id",
5990
+ "key": "id"
5991
+ }
5992
+ ],
5083
5993
  "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
5084
5994
  },
5085
5995
  {
@@ -5187,6 +6097,12 @@
5187
6097
  "name": "site-id",
5188
6098
  "key": "siteId",
5189
6099
  "required": true,
6100
+ "aliases": [
6101
+ {
6102
+ "name": "id",
6103
+ "key": "id"
6104
+ }
6105
+ ],
5190
6106
  "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
5191
6107
  },
5192
6108
  {
@@ -5297,6 +6213,12 @@
5297
6213
  "name": "site-id",
5298
6214
  "key": "siteId",
5299
6215
  "required": true,
6216
+ "aliases": [
6217
+ {
6218
+ "name": "id",
6219
+ "key": "id"
6220
+ }
6221
+ ],
5300
6222
  "description": "SharePoint site ID."
5301
6223
  },
5302
6224
  {
@@ -5419,6 +6341,12 @@
5419
6341
  "name": "site-id",
5420
6342
  "key": "siteId",
5421
6343
  "required": true,
6344
+ "aliases": [
6345
+ {
6346
+ "name": "id",
6347
+ "key": "id"
6348
+ }
6349
+ ],
5422
6350
  "description": "SharePoint site ID."
5423
6351
  },
5424
6352
  {
@@ -5468,6 +6396,12 @@
5468
6396
  "name": "site-id",
5469
6397
  "key": "siteId",
5470
6398
  "required": true,
6399
+ "aliases": [
6400
+ {
6401
+ "name": "id",
6402
+ "key": "id"
6403
+ }
6404
+ ],
5471
6405
  "description": "SharePoint site ID. Returned by `search-sharepoint-sites-by-name`."
5472
6406
  },
5473
6407
  {
@@ -5498,6 +6432,12 @@
5498
6432
  "name": "site-id",
5499
6433
  "key": "siteId",
5500
6434
  "required": true,
6435
+ "aliases": [
6436
+ {
6437
+ "name": "id",
6438
+ "key": "id"
6439
+ }
6440
+ ],
5501
6441
  "description": "SharePoint site ID."
5502
6442
  },
5503
6443
  {
@@ -5547,6 +6487,12 @@
5547
6487
  "name": "calendar-id",
5548
6488
  "key": "calendarId",
5549
6489
  "required": true,
6490
+ "aliases": [
6491
+ {
6492
+ "name": "id",
6493
+ "key": "id"
6494
+ }
6495
+ ],
5550
6496
  "description": "Calendar ID, or the well-known short name `primary` / `default` for the signed-in user’s default calendar. Use `ask-marcel list-calendars` to discover non-default calendar IDs."
5551
6497
  },
5552
6498
  {
@@ -5602,18 +6548,36 @@
5602
6548
  "name": "calendar-id",
5603
6549
  "key": "calendarId",
5604
6550
  "required": true,
6551
+ "aliases": [
6552
+ {
6553
+ "name": "id",
6554
+ "key": "id"
6555
+ }
6556
+ ],
5605
6557
  "description": "Calendar ID, or `primary` / `default` for the signed-in user’s default calendar. Returned by `ask-marcel list-calendars`."
5606
6558
  },
5607
6559
  {
5608
6560
  "name": "start-date-time",
5609
6561
  "key": "startDateTime",
5610
6562
  "required": true,
6563
+ "aliases": [
6564
+ {
6565
+ "name": "start",
6566
+ "key": "start"
6567
+ }
6568
+ ],
5611
6569
  "description": "Lower bound. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
5612
6570
  },
5613
6571
  {
5614
6572
  "name": "end-date-time",
5615
6573
  "key": "endDateTime",
5616
6574
  "required": true,
6575
+ "aliases": [
6576
+ {
6577
+ "name": "end",
6578
+ "key": "end"
6579
+ }
6580
+ ],
5617
6581
  "description": "Upper bound. ISO 8601 UTC (e.g. `2026-04-01T00:00:00Z` or `2026-04-01`) OR a relative shape: `7d` / `1w` / `2h` / `30m` (past), `+7d` (future), `today` / `yesterday` / `tomorrow` / `now`, `monday`-`sunday` (most recent), `last-<weekday>` / `next-<weekday>`, `start-of-week|month|year`, `end-of-week|month|year`. Relative forms resolve at request time relative to the CLI process clock (UTC)."
5618
6582
  },
5619
6583
  {
@@ -5668,6 +6632,12 @@
5668
6632
  "name": "team-id",
5669
6633
  "key": "teamId",
5670
6634
  "required": true,
6635
+ "aliases": [
6636
+ {
6637
+ "name": "id",
6638
+ "key": "id"
6639
+ }
6640
+ ],
5671
6641
  "description": "Microsoft Teams team ID. Returned by `ask-marcel list-joined-teams`."
5672
6642
  },
5673
6643
  {
@@ -5698,6 +6668,12 @@
5698
6668
  "name": "team-id",
5699
6669
  "key": "teamId",
5700
6670
  "required": true,
6671
+ "aliases": [
6672
+ {
6673
+ "name": "id",
6674
+ "key": "id"
6675
+ }
6676
+ ],
5701
6677
  "description": "Microsoft Teams team ID."
5702
6678
  }
5703
6679
  ],
@@ -5717,6 +6693,12 @@
5717
6693
  "name": "chat-id",
5718
6694
  "key": "chatId",
5719
6695
  "required": true,
6696
+ "aliases": [
6697
+ {
6698
+ "name": "id",
6699
+ "key": "id"
6700
+ }
6701
+ ],
5720
6702
  "description": "Teams chat ID — typically `19:<thread>@unq.gbl.spaces` (1:1) or `19:<thread>@thread.v2` (group). Source via `list-chats` or `list-teams-chats-with-messages`."
5721
6703
  },
5722
6704
  {
@@ -5752,6 +6734,7 @@
5752
6734
  ],
5753
6735
  "example": "ask-marcel list-teams-chat-history --chat-id '19:abc...@unq.gbl.spaces' --max-pages 5",
5754
6736
  "responseShape": "`{ messages: [...], hasMore: boolean, pagesFetched: number, nextSyncState?: string, projection: 'slim' | 'full' }`. Slim projection (default) emits `{ id, sequenceId, composetime, originalarrivaltime, messagetype, from, imdisplayname, content }` per message, with `truncated: true` + `originalContentChars` on entries whose `content` exceeded `--max-content-chars` (default 4096). With `--full true`, returns the raw IC3 substrate shape: `id`, `sequenceId` (monotonic per-chat counter), `composetime`, `originalarrivaltime`, `messagetype`, `content`, `from`, `imdisplayname`, `properties.subject`, etc. **`hasMore: true`** means the safety cap was hit and there is older history beyond what was returned — chain a follow-up call with `--sync-state $(jq -r .data.nextSyncState <prev>)` to continue. **`hasMore: false`** means the chat's earliest message was reached. **Microsoft-internal schema — fields may change without notice.**",
6737
+ "needsSubstrateToken": true,
5755
6738
  "stability": "experimental"
5756
6739
  },
5757
6740
  {
@@ -5766,11 +6749,18 @@
5766
6749
  "name": "chat-id",
5767
6750
  "key": "chatId",
5768
6751
  "required": true,
6752
+ "aliases": [
6753
+ {
6754
+ "name": "id",
6755
+ "key": "id"
6756
+ }
6757
+ ],
5769
6758
  "description": "Teams chat ID — typically `19:<thread>@unq.gbl.spaces` (1:1) or `19:<thread>@thread.v2` (group). Source via `list-chats` or `list-teams-chats-with-messages`."
5770
6759
  }
5771
6760
  ],
5772
6761
  "example": "ask-marcel list-teams-chat-messages --chat-id '19:abc...@unq.gbl.spaces'",
5773
6762
  "responseShape": "Substrate envelope: `{ messages: [...], messageToken: string }`. Returns up to the 200 most recent messages per chat — older history is NOT reachable via this endpoint. Each message has `id`, `from`, `imDisplayName`, `content`, `contentType`, `composeTime`, `originalArrivalTime`, `sequenceId`, etc. `messageToken` is returned for forward compatibility but is currently a static snapshot identifier (server ignores it as a pagination cursor). **Microsoft-internal schema — fields may change without notice.** For history older than the 200 most recent, use `list-teams-chat-history` (rides the IC3 substrate with a working syncState cursor).",
6763
+ "needsSubstrateToken": true,
5774
6764
  "stability": "experimental"
5775
6765
  },
5776
6766
  {
@@ -5796,6 +6786,7 @@
5796
6786
  ],
5797
6787
  "example": "ask-marcel list-teams-chats-with-messages --page-size 100",
5798
6788
  "responseShape": "`{ chats: [...], continuationToken?: string, hasMoreData?: boolean }`. Each chat carries `id`, `title`, `chatType`, `threadType`, `members[]` (with each member's `mri`, `displayName`, `email`), `createdAt`, AND `lastMessage` (the most recent message body inlined — `content`, `from`, `composeTime`, `imDisplayName`, etc.). When `hasMoreData: true`, chain a follow-up call with `--continuation-token \"$(jq -r .data.continuationToken <prev>)\"`. **Microsoft-internal schema — fields may change without notice; treat the response as semi-structured.**",
6789
+ "needsSubstrateToken": true,
5799
6790
  "stability": "experimental"
5800
6791
  },
5801
6792
  {
@@ -5926,6 +6917,10 @@
5926
6917
  "required": true,
5927
6918
  "description": "To Do task list ID. Returned by `ask-marcel list-todo-task-lists`.",
5928
6919
  "aliases": [
6920
+ {
6921
+ "name": "id",
6922
+ "key": "id"
6923
+ },
5929
6924
  {
5930
6925
  "name": "task-list-id",
5931
6926
  "key": "taskListId"
@@ -5991,6 +6986,10 @@
5991
6986
  "required": true,
5992
6987
  "description": "Microsoft To Do task list ID. Returned by `list-todo-task-lists`.",
5993
6988
  "aliases": [
6989
+ {
6990
+ "name": "id",
6991
+ "key": "id"
6992
+ },
5994
6993
  {
5995
6994
  "name": "task-list-id",
5996
6995
  "key": "taskListId"
@@ -6067,6 +7066,12 @@
6067
7066
  "name": "user-id",
6068
7067
  "key": "userId",
6069
7068
  "required": true,
7069
+ "aliases": [
7070
+ {
7071
+ "name": "id",
7072
+ "key": "id"
7073
+ }
7074
+ ],
6070
7075
  "description": "Azure AD user ID or userPrincipalName (UPN) — typically the user's email address. Discover via `list-relevant-people` (relevance-ranked colleagues) or `microsoft-search-query --query <name>` (federated person search across the tenant directory)."
6071
7076
  },
6072
7077
  {
@@ -6131,7 +7136,7 @@
6131
7136
  },
6132
7137
  {
6133
7138
  "name": "my-quick-context",
6134
- "summary": "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.",
7139
+ "summary": "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.",
6135
7140
  "category": "meta",
6136
7141
  "graphMethod": "GET",
6137
7142
  "graphPathTemplate": "(meta) parallel: /me, /me/drive, /me/mailFolders/inbox, /me/calendar, /me/planner/plans, /me/onenote/notebooks, /me/joinedTeams, /me/drive/recent, /me/mailboxSettings",
@@ -6161,6 +7166,44 @@
6161
7166
  "example": "ask-marcel next-page --url 'https://graph.microsoft.com/v1.0/me/messages?$skip=10'",
6162
7167
  "responseShape": "same shape as the originating endpoint — `{ ok: true, data: { value: [...] }, nextLink: \"...\" }` with the cursor at envelope level."
6163
7168
  },
7169
+ {
7170
+ "name": "read-mail-attachment",
7171
+ "summary": "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.",
7172
+ "category": "mail",
7173
+ "graphMethod": "GET",
7174
+ "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
7175
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
7176
+ "options": [
7177
+ {
7178
+ "name": "message-id",
7179
+ "key": "messageId",
7180
+ "required": true,
7181
+ "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
7182
+ },
7183
+ {
7184
+ "name": "attachment-id",
7185
+ "key": "attachmentId",
7186
+ "required": true,
7187
+ "description": "Attachment ID inside that message. Returned by `list-mail-attachments`."
7188
+ },
7189
+ {
7190
+ "name": "include-metadata",
7191
+ "key": "includeMetadata",
7192
+ "required": false,
7193
+ "description": "Pass `--include-metadata true` to append each converted Office file’s side-channel metadata block (docx / xlsx / pptx / OpenDocument). No-op on images, embedded items, and plain text.",
7194
+ "argumentHint": {
7195
+ "kind": "magicValue",
7196
+ "values": [
7197
+ "true",
7198
+ "false"
7199
+ ]
7200
+ }
7201
+ }
7202
+ ],
7203
+ "example": "ask-marcel read-mail-attachment --message-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1'",
7204
+ "responseShape": "Polymorphic by attachment content-type. A zip → `{ count, files: [{ path, contentType, size, text } | { path, note }], truncated? }` (the convert-mail-attachment-zip shape). Everything else → `{ contentType: \"text/markdown\" | \"text/plain\", size, text, pageCount? }` (the convert-mail-attachment-to-markdown shape; `pageCount` present for PDF sources). Unsupported types (image / scanned PDF / legacy .ppt) return an api_error (415/400) naming the right next command.",
7205
+ "producesBytes": true
7206
+ },
6164
7207
  {
6165
7208
  "name": "resolve-calendar-link",
6166
7209
  "summary": "Parse a Microsoft Outlook calendar item link (the URL emitted by the \"Copy link\" / share action on a calendar event) into its `eventId`. Pure transformation — no Graph call. Pipe the result into `get-calendar-event` to fetch the event body. For Outlook mail message links use `resolve-mail-link` instead — this command rejects them with a pointer.",
@@ -6181,7 +7224,7 @@
6181
7224
  },
6182
7225
  {
6183
7226
  "name": "resolve-drive-share-link",
6184
- "summary": "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`.",
7227
+ "summary": "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`.",
6185
7228
  "category": "drive",
6186
7229
  "graphMethod": "GET",
6187
7230
  "graphPathTemplate": "{url}",
@@ -6235,7 +7278,7 @@
6235
7278
  },
6236
7279
  {
6237
7280
  "name": "scopes-check",
6238
- "summary": "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.",
7281
+ "summary": "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.",
6239
7282
  "category": "meta",
6240
7283
  "graphMethod": "GET",
6241
7284
  "graphPathTemplate": "(meta) cached-token introspection — no Graph endpoint",
@@ -6244,6 +7287,31 @@
6244
7287
  "example": "ask-marcel scopes-check",
6245
7288
  "responseShape": "`{ scopes: string[], audience: string, expiresAt: string (ISO 8601), expiresInSeconds: number }`. `expiresInSeconds` is negative when the cached token has already expired (run `login`); `audience` is the JWT `aud` claim (typically `https://graph.microsoft.com`)."
6246
7289
  },
7290
+ {
7291
+ "name": "search-all-accessible-sites",
7292
+ "summary": "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.",
7293
+ "category": "sharepoint",
7294
+ "graphMethod": "POST",
7295
+ "graphPathTemplate": "/search/query",
7296
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/search-query",
7297
+ "options": [
7298
+ {
7299
+ "name": "query",
7300
+ "key": "query",
7301
+ "required": false,
7302
+ "description": "Optional KQL filter applied to the site index (default `*` = every site you can access). Examples: a name fragment like `budget`, or `contentclass:STS_Site` to restrict to site collections. Free text is matched against site title/url."
7303
+ },
7304
+ {
7305
+ "name": "count-files",
7306
+ "key": "countFiles",
7307
+ "required": false,
7308
+ "description": "Pass `--count-files true` to add `estimatedFileCount` to each kept site — the Microsoft Search index's security-trimmed `driveItem` total (files + folders) scoped to that site's `webUrl` via KQL `path:`. OFF by default because it issues ONE extra Search query per site (chunked, capped at 200) — a real fan-out with 429-throttling risk; narrow with `--query` first. It is an estimate, not an exact count."
7309
+ }
7310
+ ],
7311
+ "example": "ask-marcel search-all-accessible-sites --output json",
7312
+ "responseShape": "`{ value: [<Microsoft Graph site resource: { id, name, displayName?, webUrl, size?, estimatedFileCount?, … }>], count, fileEstimate?, archivedExcluded?, nonNavigableExcluded?, notFoundExcluded?, archiveProbeErrors?, truncated?: true, archiveProbeTruncated?: true }`. `estimatedFileCount` appears only with `--count-files true` — the security-trimmed `driveItem` (files+folders) estimate scoped to each site, omitted past the 200-site count cap or when the per-site query fails. `value[]` is deduped by site `id` across pages; `count` is the number of distinct sites returned (the authoritative figure). `size` (when present) is the site's DEFAULT document library's total bytes used (`drive.quota.used`, recursive) — folded onto the same per-site probe for free, omitted for sites past the probe ceiling or without a default drive; it is a data-volume signal, not a file count. `fileEstimate` (best-effort, omitted if the extra query fails) is the Microsoft Search index's security-trimmed `driveItem` count — roughly how many files+folders the user can access across all of SharePoint/OneDrive. `truncated: true` means paging stopped early (page ceiling hit, or a later page errored) — narrow with `--query` to see the rest; its absence means the sweep ran to completion. `archivedExcluded` (omitted when 0) counts sites dropped as archived/locked by the per-site probe; `nonNavigableExcluded` counts sites dropped by URL shape (add-in app domains, `/contentstorage/` SharePoint Embedded containers, `/_layouts/` system pages); `notFoundExcluded` counts sites whose probe returned 404. `archiveProbeErrors` (omitted when 0) counts sites whose probe failed for an unrelated reason — those are KEPT, so an archived one could slip through. `archiveProbeTruncated: true` means more sites were returned than the probe ceiling (250); the overflow is kept unprobed — narrow with `--query`.",
7313
+ "bodyTemplate": "{ requests: [{ entityTypes: ['site'], query: { queryString: '{query}' }, from: <page*25>, size: 25 }] } — `{query}` defaults to `*` (all accessible sites); re-issued per page, advancing `from` by 25 until `moreResultsAvailable` is false"
7314
+ },
6247
7315
  {
6248
7316
  "name": "search-mail-messages",
6249
7317
  "summary": "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.",
@@ -6360,7 +7428,13 @@
6360
7428
  "name": "drive-id",
6361
7429
  "key": "driveId",
6362
7430
  "required": true,
6363
- "description": "Microsoft Graph drive ID to search inside. Returned by `ask-marcel list-drives`."
7431
+ "aliases": [
7432
+ {
7433
+ "name": "id",
7434
+ "key": "id"
7435
+ }
7436
+ ],
7437
+ "description": "Microsoft Graph drive ID to search inside. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library."
6364
7438
  },
6365
7439
  {
6366
7440
  "name": "query",
@@ -6510,8 +7584,94 @@
6510
7584
  }
6511
7585
  ],
6512
7586
  "example": "ask-marcel search-sharepoint-sites-by-name --query 'marketing'",
6513
- "responseShape": "collection of Microsoft Graph `site` resources under `value[]` (up to 25)",
7587
+ "responseShape": "collection of Microsoft Graph `site` resources under `value[]` (up to 25), each carrying `size?` — its default document library's total bytes used (`drive.quota.used`, recursive), folded onto the same probe for free (a data-volume signal, not a file count; omitted when the site has no default drive). Sites you cannot open are excluded: `nonNavigableExcluded` drops add-in app domains, `/contentstorage/` (SharePoint Embedded) containers, and `/_layouts/` system URLs by URL shape (no probe); each remaining match is probed (`GET /sites/{id}?$select=…,siteCollection&$expand=drive($select=quota)`) and `archivedExcluded` drops archived / `423 resourceLocked` sites while `notFoundExcluded` drops probes that 404. All three counters are omitted when 0. `archiveProbeErrors` (omitted when 0) counts matches kept because their probe failed for an unrelated reason. Active personal OneDrives are kept.",
6514
7588
  "pagination": true
7589
+ },
7590
+ {
7591
+ "name": "update-mail-draft",
7592
+ "summary": "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.",
7593
+ "category": "mail",
7594
+ "graphMethod": "PATCH",
7595
+ "graphPathTemplate": "/me/messages/{message-id}",
7596
+ "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-update",
7597
+ "options": [
7598
+ {
7599
+ "name": "message-id",
7600
+ "key": "messageId",
7601
+ "required": true,
7602
+ "aliases": [
7603
+ {
7604
+ "name": "id",
7605
+ "key": "id"
7606
+ }
7607
+ ],
7608
+ "description": "Draft message ID to update. Source from create-mail-draft response or list-mail-folder-messages --mail-folder-id drafts. Accepts `--id` as an alias.",
7609
+ "argumentHint": {
7610
+ "kind": "idOrName"
7611
+ }
7612
+ },
7613
+ {
7614
+ "name": "subject",
7615
+ "key": "subject",
7616
+ "required": false,
7617
+ "description": "New email subject line. Omit to keep the current subject."
7618
+ },
7619
+ {
7620
+ "name": "body-content",
7621
+ "key": "bodyContent",
7622
+ "required": false,
7623
+ "description": "New email body content. Replaces the entire body. Pass --body-content-type HTML for rich text."
7624
+ },
7625
+ {
7626
+ "name": "body-content-type",
7627
+ "key": "bodyContentType",
7628
+ "required": false,
7629
+ "description": "Body format for the new body: Text (default) or HTML. Only used when --body-content is provided.",
7630
+ "argumentHint": {
7631
+ "kind": "magicValue",
7632
+ "values": [
7633
+ "Text",
7634
+ "HTML"
7635
+ ]
7636
+ }
7637
+ },
7638
+ {
7639
+ "name": "to-recipients",
7640
+ "key": "toRecipients",
7641
+ "required": false,
7642
+ "description": "Comma-separated list of recipient email addresses. Replaces the entire toRecipients list."
7643
+ },
7644
+ {
7645
+ "name": "cc-recipients",
7646
+ "key": "ccRecipients",
7647
+ "required": false,
7648
+ "description": "Comma-separated list of CC recipient email addresses. Replaces the entire ccRecipients list."
7649
+ },
7650
+ {
7651
+ "name": "bcc-recipients",
7652
+ "key": "bccRecipients",
7653
+ "required": false,
7654
+ "description": "Comma-separated list of BCC recipient email addresses. Replaces the entire bccRecipients list."
7655
+ },
7656
+ {
7657
+ "name": "importance",
7658
+ "key": "importance",
7659
+ "required": false,
7660
+ "description": "Email importance: Low, Normal, or High.",
7661
+ "argumentHint": {
7662
+ "kind": "magicValue",
7663
+ "values": [
7664
+ "Low",
7665
+ "Normal",
7666
+ "High"
7667
+ ]
7668
+ }
7669
+ }
7670
+ ],
7671
+ "example": "ask-marcel update-mail-draft --message-id \"AAMkAD...\" --subject \"Updated: Q3 Report\" --to-recipients \"alice@example.com,charlie@example.com\"",
7672
+ "responseShape": "The updated Microsoft Graph message object: `{ id, subject, body, from, toRecipients, ccRecipients, bccRecipients, receivedDateTime, isDraft, … }`. Graph returns 204 No Content on success with no body — the CLI surfaces `{ ok: true }` in that case.",
7673
+ "bodyTemplate": "{ subject?: '{subject}', body?: { contentType: '{body-content-type}', content: '{body-content}' }, toRecipients?: '{to-recipients}', ccRecipients?: '{cc-recipients}', bccRecipients?: '{bcc-recipients}', importance?: '{importance}' } — only provided fields are sent",
7674
+ "mutates": true
6515
7675
  }
6516
7676
  ]
6517
7677
  }