{
  "package": "ask-marcel-office-cli",
  "version": "1.4.0",
  "generatedAt": "2026-05-27T00:44:06.845Z",
  "commands": [
    {
      "name": "convert-mail-attachment-to-markdown",
      "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).",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
        },
        {
          "name": "attachment-id",
          "key": "attachmentId",
          "required": true,
          "description": "Attachment ID inside that message. Returned by `list-mail-attachments`."
        }
      ],
      "example": "ask-marcel convert-mail-attachment-to-markdown --message-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1'",
      "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."
    },
    {
      "name": "convert-mail-attachment-to-pdf",
      "summary": "Convert an Outlook mail attachment to PDF on the fly. Polymorphic on the attachment’s `@odata.type`: fileAttachment uploads the bytes to a temp folder under /me/drive (large files use Graph’s chunked upload session — no 4 MB ceiling), runs ?format=pdf, then deletes the temp item; referenceAttachment resolves via /shares/{token}/driveItem and runs ?format=pdf in place; plain-text source extensions and `pdf` sources short-circuit to a raw-bytes envelope on either path (Graph’s `?format=pdf` does not accept `pdf` as an input format — pdf attachments are returned as-is). itemAttachment (embedded mail/event/contact) is unsupported here — Graph rejects those source types — use convert-mail-attachment-to-markdown instead. Worst-case wall-clock for huge attachments is ~22 minutes (1 metadata GET + up-to-20 chunk PUTs + 1 convert GET + 1 cleanup DELETE, each capped at 60s).",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
        },
        {
          "name": "attachment-id",
          "key": "attachmentId",
          "required": true,
          "description": "Attachment ID inside that message. Returned by `list-mail-attachments`."
        }
      ],
      "example": "ask-marcel convert-mail-attachment-to-pdf --message-id 'AAMkAD...' --attachment-id 'AAMkAD...attach1'",
      "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."
    },
    {
      "name": "convert-mail-to-markdown",
      "summary": "Render a single Outlook email as markdown — headers (`**Subject:**`, `**From:**`, `**To:**`, `**Cc:**` only when present, `**Date:**`), followed by the body run through turndown. By default, inline images (`isInline:true` + `image/*` content-type, size ≤ 2 MB) are embedded as base64 `data:` URIs so the output is self-contained (non-image inline attachments are NOT embedded; oversize inline images are replaced with a placeholder note). For LLM callers that only want the text body, pass `--inline-images false` to skip the per-image bytes fetch entirely — the body keeps raw `cid:<contentId>` references and the inline images surface in the file-attachments list so you can decide whether to fetch them separately via `get-mail-attachment`. File attachments are always listed below the body by name + size + id; their bytes are NOT fetched here — call `convert-mail-attachment-to-pdf` or `get-mail-attachment` with the id when you actually need them. Staged-fetch design (audit v1.0.0): one call for the body, one for the attachments-metadata list (only if `hasAttachments:true`), and one per small inline image — replaces the old `?$expand=attachments` which timed out / truncated on messages with multi-MB attachments.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-get",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
        },
        {
          "name": "inline-images",
          "key": "inlineImages",
          "required": false,
          "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."
        }
      ],
      "example": "ask-marcel convert-mail-to-markdown --message-id 'AAMkAD...'",
      "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."
    },
    {
      "name": "download-drive-item-as-markdown",
      "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.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content?format=html",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get-content-format",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "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."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the file to convert. Returned by `list-folder-files` or `search-onedrive-files`."
        }
      ],
      "example": "ask-marcel download-drive-item-as-markdown --drive-id 'b!1234' --item-id '01ABC'",
      "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."
    },
    {
      "name": "download-drive-item-as-pdf",
      "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.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content?format=pdf",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get-content-format",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "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."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the file to convert. Returned by `list-folder-files` or `search-onedrive-files`."
        }
      ],
      "example": "ask-marcel download-drive-item-as-pdf --drive-id 'b!1234' --item-id '01ABC'",
      "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."
    },
    {
      "name": "download-drive-item-version",
      "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).",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/versions/{version-id}/content",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitemversion-get-content",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "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."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the file. Returned by `list-folder-files` or `search-onedrive-files`."
        },
        {
          "name": "version-id",
          "key": "versionId",
          "required": true,
          "description": "driveItemVersion ID. Returned by `ask-marcel list-drive-item-versions`. Use the `id` field of an entry under `value[]`. Pick a non-current version — the first entry (e.g. `12.0`) is the live file and Graph rejects this endpoint for it; use `value[1]` or older."
        },
        {
          "name": "format",
          "key": "format",
          "required": false,
          "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.",
          "argumentHint": {
            "kind": "magicValue",
            "values": [
              "original",
              "pdf",
              "markdown"
            ]
          }
        }
      ],
      "example": "ask-marcel download-drive-item-version --drive-id 'b!1234' --item-id '01ABC' --version-id '4.0' --format pdf",
      "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."
    },
    {
      "name": "download-onedrive-file-content",
      "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.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/content",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get-content",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the file to download. Returned by `ask-marcel list-folder-files` or `search-onedrive-files`."
        }
      ],
      "example": "ask-marcel download-onedrive-file-content --drive-id 'b!1234' --item-id '01ABC'",
      "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."
    },
    {
      "name": "extract-sharepoint-links-in-mail",
      "summary": "Find every `*.sharepoint.com` URL in the body of a single Outlook email and resolve each one to its driveItem (driveId, itemId, name, webUrl) so the agent can feed those into `download-drive-item-as-pdf` / `-as-markdown` etc. Read-only — no conversion happens here. Capped at 25 unique URLs per call to bound fan-out (returns `truncated: true` and `skippedCount` when the body has more); duplicate URLs are deduplicated. Per-link errors are captured inside each entry instead of failing the whole call.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-get",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `list-mail-messages` or `list-mail-folder-messages`."
        }
      ],
      "example": "ask-marcel extract-sharepoint-links-in-mail --message-id 'AAMkADk0...'",
      "responseShape": "`{ messageId, subject, links: [{ url, driveId, itemId, name, webUrl } | { url, error }], truncated, skippedCount }` — one entry per unique SharePoint URL found in the body, ordered by first occurrence."
    },
    {
      "name": "find-chats-with-user",
      "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.",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chat-list",
      "options": [
        {
          "name": "name",
          "key": "name",
          "required": true,
          "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."
        },
        {
          "name": "max-pages",
          "key": "maxPages",
          "required": false,
          "description": "Safety cap on the chat-list walk (positive integer; default 10). Each page returns up to `--page-size` chats. Raise carefully on busy accounts — every page is one HTTP round-trip."
        },
        {
          "name": "page-size",
          "key": "pageSize",
          "required": false,
          "description": "Chats per page (positive integer; default 100, same value Teams web uses). Server may silently cap."
        }
      ],
      "example": "ask-marcel find-chats-with-user --name 'Hervé ATTANE'",
      "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`).",
      "stability": "experimental"
    },
    {
      "name": "get-calendar-event",
      "summary": "Fetch a single calendar event by ID from the signed-in user’s default calendar. Pass `--select` to project only the fields you need (the full event body can be large with HTML body and attendee lists).",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/events/{event-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/event-get",
      "options": [
        {
          "name": "event-id",
          "key": "eventId",
          "required": true,
          "description": "Microsoft Graph event ID. Returned by `ask-marcel list-calendar-events` in the `id` field of each event."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-calendar-event --event-id 'AAMkAGI2THVS...' --select id,subject,start,end,attendees",
      "responseShape": "single Microsoft Graph `event` resource (or projection of the requested `--select` fields)"
    },
    {
      "name": "get-channel-files-folder",
      "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`.",
      "category": "teams",
      "graphMethod": "GET",
      "graphPathTemplate": "/teams/{team-id}/channels/{channel-id}/filesFolder",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/channel-get-filesfolder",
      "options": [
        {
          "name": "team-id",
          "key": "teamId",
          "required": true,
          "description": "Microsoft Teams team ID. Returned by `list-joined-teams`."
        },
        {
          "name": "channel-id",
          "key": "channelId",
          "required": true,
          "description": "Channel ID inside the team. Returned by `list-team-channels`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-channel-files-folder --team-id 'tm1' --channel-id 'ch1'",
      "responseShape": "single Microsoft Graph `driveItem` resource (folder)"
    },
    {
      "name": "get-chat",
      "summary": "Return metadata for a single Microsoft Teams chat (1:1, group, or meeting). The CLI ships a slim default `--select=id,topic,chatType,createdDateTime,lastUpdatedDateTime`; pass `--select id,topic,webUrl,onlineMeetingInfo` (or any other comma-separated field list) to widen. Pass `--expand members` to inline membership. Returns metadata only — not the messages (which need `Chat.Read*`). Requires the M365ChatClient elevated token captured at login (the basic Teams web client token lacks `Chat.ReadBasic`).",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "/chats/{chat-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chat-get",
      "options": [
        {
          "name": "chat-id",
          "key": "chatId",
          "required": true,
          "description": "Microsoft Teams chat ID, e.g. `19:abc...@thread.v2`. Returned by `list-chats`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-chat --chat-id '19:abc...@thread.v2'",
      "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."
    },
    {
      "name": "get-current-user",
      "summary": "Return the signed-in user's Microsoft Graph profile. The CLI ships a slim default `--select=id,displayName,mail,userPrincipalName,jobTitle,officeLocation,mobilePhone` covering the common identity fields. Pass `--select id,displayName,givenName,surname,preferredLanguage,...` to widen, or `--select '*'` for everything Graph returns.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-get",
      "options": [
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-current-user",
      "responseShape": "single Microsoft Graph `user` resource projected to the default `--select` set (or, when overridden, to the requested fields)"
    },
    {
      "name": "get-drive-delta",
      "summary": "Get the incremental change set (added / modified / deleted items) under a OneDrive / SharePoint folder. Use the `@odata.deltaLink` from a previous response to resume.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/delta()",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-delta",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the folder whose subtree to track. Use the root folder ID from `get-drive-root-item` to track the entire drive. Accepts `--folder-id` as an alias for parity with `list-folder-files` (same concept, same flag name).",
          "aliases": [
            {
              "name": "folder-id",
              "key": "folderId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-delta --drive-id 'b!1234' --item-id '01ROOT'",
      "responseShape": "collection of changed Microsoft Graph `driveItem` resources under `data.value[]`. Cursor tokens are hoisted to envelope level: top-level `nextLink` while paging, then top-level `deltaLink` on the final page.",
      "pagination": true
    },
    {
      "name": "get-drive-item",
      "summary": "Get the metadata (driveItem resource) of a single file or folder in OneDrive / SharePoint. Use `--select` to slim the response — a full driveItem can run >10 KB with all the optional facets.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID. Returned by `list-folder-files`, `search-onedrive-files`, or `get-drive-root-item`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-item --drive-id 'b!1234' --item-id '01ABC' --select 'id,name,size,lastModifiedDateTime'",
      "responseShape": "single Microsoft Graph `driveItem` resource"
    },
    {
      "name": "get-drive-item-analytics",
      "summary": "Return view / activity analytics for a OneDrive / SharePoint file — `allTime` totals (views, viewers) and `lastSevenDays` rollup. Useful for ranking files by attention or detecting stale content. **Known empty case**: returns `{ allTime: null, lastSevenDays: null }` on low-traffic items, or when the calling identity (the Teams web client basic token) lacks the analytics scope on the tenant. Do not interpret nulls as \"no views\" — interpret as \"not available for this caller\". For active files where you expect data and see nulls, escalate to a token with `Reports.Read.All`.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/analytics",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/itemanalytics-get",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID."
        }
      ],
      "example": "ask-marcel get-drive-item-analytics --drive-id 'b!1234' --item-id '01ABC'",
      "responseShape": "single Microsoft Graph `itemAnalytics` resource"
    },
    {
      "name": "get-drive-item-created-by-user",
      "summary": "Return the `user` resource for whoever created a OneDrive / SharePoint file — full profile, not just the truncated `createdBy.user` summary embedded in the parent driveItem. Useful when you need title / department / mail of the author. Use `--select` to fetch only the fields you care about (e.g. `--select id,displayName,jobTitle,department,mail`).",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/createdByUser",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-item-created-by-user --drive-id 'b!1234' --item-id '01ABC' --select 'id,displayName,jobTitle,mail'",
      "responseShape": "single Microsoft Graph `user` resource"
    },
    {
      "name": "get-drive-item-last-modified-by-user",
      "summary": "Return the full `user` resource for whoever last modified a OneDrive / SharePoint file — sibling to `get-drive-item-created-by-user`. Use `--select` to fetch only specific fields.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/lastModifiedByUser",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-item-last-modified-by-user --drive-id 'b!1234' --item-id '01ABC' --select 'id,displayName,mail'",
      "responseShape": "single Microsoft Graph `user` resource"
    },
    {
      "name": "get-drive-item-list-item",
      "summary": "Return the SharePoint listItem projection of a OneDrive / SharePoint file — exposes the file's library-defined column values (custom metadata: status, due-date, classification, taxonomy tags, etc.) which are NOT present on the plain `driveItem`. Combine with `list-sharepoint-list-columns` to interpret the column schema.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/listItem",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/listitem-get",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID. Returned by `list-folder-files` or `search-onedrive-files`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-item-list-item --drive-id 'b!1234' --item-id '01ABC'",
      "responseShape": "single Microsoft Graph `listItem` resource"
    },
    {
      "name": "get-drive-root-delta",
      "summary": "Track incremental changes (added / modified / deleted items) anywhere under the signed-in user's OneDrive root. **Takes zero required arguments** — acts implicitly on the signed-in user's primary OneDrive; use `get-drive-delta` to target a specific drive by ID. The first call returns a snapshot plus `@odata.deltaLink`; subsequent calls with that link return only what has changed since. Cross-folder companion to `get-drive-delta` (which scopes to one specific folder).",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/drive/root/delta()",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-delta",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-root-delta",
      "responseShape": "collection of Microsoft Graph `driveItem` resources under `data.value[]`. Cursor tokens are hoisted to envelope level: top-level `nextLink` while paging, then top-level `deltaLink` on the final page.",
      "pagination": true
    },
    {
      "name": "get-drive-root-item",
      "summary": "Get the root folder (driveItem) of a OneDrive / SharePoint drive. Use `--select` to slim the response (e.g. `--select id,name,folder`).",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/root",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-get",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives` in the `id` field."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-root-item --drive-id 'b!1234'",
      "responseShape": "single Microsoft Graph `driveItem` resource (the root folder)"
    },
    {
      "name": "get-drive-special-folder",
      "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`.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/drive/special/{folder-name}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-get-specialfolder",
      "options": [
        {
          "name": "folder-name",
          "key": "folderName",
          "required": true,
          "description": "Well-known folder name. One of: `documents`, `photos`, `cameraroll`, `approot`, `music`, `attachments`. Returns the corresponding driveItem (folder) — use the returned `id` with `list-folder-files` etc."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-drive-special-folder --folder-name 'documents'",
      "responseShape": "single Microsoft Graph `driveItem` resource (folder)"
    },
    {
      "name": "get-excel-range",
      "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.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/range(address='{address}')",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/worksheet-range",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "worksheet-id",
          "key": "worksheetId",
          "required": true,
          "description": "Worksheet ID or worksheet name. Returned by `ask-marcel list-excel-worksheets`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "address",
          "key": "address",
          "required": true,
          "description": "A1-style range address, e.g. `A1:C10` or a single cell like `B7`. The CLI rejects ranges spanning more than 100 000 cells client-side to prevent runaway responses. Do NOT prefix with the worksheet name — `--worksheet-id` already pins the sheet, and a cross-sheet prefix like `OtherSheet!A1:C2` is rejected by Graph.",
          "argumentHint": {
            "kind": "a1Address"
          }
        }
      ],
      "example": "ask-marcel get-excel-range --drive-id 'b!1234' --item-id '01XLSX' --worksheet-id 'Sheet1' --address 'A1:C10'",
      "responseShape": "single Microsoft Graph `workbookRange` resource (values, formulas, format)"
    },
    {
      "name": "get-excel-table",
      "summary": "Get the metadata (style, header row, total row) of a single named Excel table.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/tables/{table-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/workbooktable-get",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "table-id",
          "key": "tableId",
          "required": true,
          "description": "Workbook table ID or table name. Returned by `ask-marcel list-excel-tables`."
        }
      ],
      "example": "ask-marcel get-excel-table --drive-id 'b!1234' --item-id '01XLSX' --table-id 'Table1'",
      "responseShape": "single Microsoft Graph `workbookTable` resource"
    },
    {
      "name": "get-excel-used-range",
      "summary": "Return the worksheet's used range — the bounding box of every non-empty cell — as a single Excel range. The CLI ships a slim default that strips the redundant `text` / `numberFormat` / `formulas` 2D arrays Graph returns (mostly `\"General\"` repeated cell-by-cell), keeping `address` / `rowCount` / `columnCount` / `values`. Pass `--full true` to return the raw four-array Graph shape. `--max-cells` (default 50 000) caps the size of the projected `values[]`; oversize ranges drop `values` and surface a hint pointing at `get-excel-range` for band-by-band reads. Avoids fetching the entire 1M × 16K-cell sheet when only a small data island is populated.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/usedRange()",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/worksheet-usedrange",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "worksheet-id",
          "key": "worksheetId",
          "required": true,
          "description": "Accepts either the worksheet display name (e.g. `Sheet1`, `PROJECT`) or the worksheet `id` GUID returned by `list-excel-worksheets`. If neither matches Graph responds `itemNotFound: The requested resource doesn't exist.` — when that happens, double-check spelling case-sensitively against `ask-marcel list-excel-worksheets --drive-id <d> --item-id <i>`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "full",
          "key": "full",
          "required": false,
          "description": "Pass `--full true` to return the raw Graph `workbookRange` shape with all four 2D arrays (`values`, `text`, `numberFormat`, `formulas`). Default (`--full false`, or omitted) drops the three redundant arrays and ships only `values`. The raw shape on a 3×148 sheet is ~125 KB (most of it duplicated `\"General\"` numberFormat strings); the slim default is ~5-15 KB."
        },
        {
          "name": "max-cells",
          "key": "maxCells",
          "required": false,
          "description": "Cap (positive integer; default 50 000) on the size of the projected `values[]` in slim mode. When the used-range exceeds the cap, the response keeps `address` / `rowCount` / `columnCount` but drops `values[]` and adds `truncated: true` plus a hint pointing at `get-excel-range` for band-by-band reads. Ignored when `--full true` is set (the caller has opted into the full payload regardless of size)."
        }
      ],
      "example": "ask-marcel get-excel-used-range --drive-id 'b!1234' --item-id '01ABC' --worksheet-id 'Sheet1'",
      "responseShape": "Slim projection (default): `{ address, rowCount, columnCount, values, projection: 'slim' }` — `values[]` is the 2D cell-value array. Oversize variant: `{ address, rowCount, columnCount, projection: 'slim', truncated: true, maxCells, hint }` (no `values`). With `--full true`: the raw Graph `workbookRange` resource (adds `text`, `numberFormat`, `formulas` 2D arrays) plus `projection: 'full'`. Workbook Online (WAC) errors are translated to a clear `item is not an accessible Excel workbook` envelope (see `excel-error.ts`)."
    },
    {
      "name": "get-group",
      "summary": "Return metadata for a single Azure AD / Microsoft 365 group. Use `--select` to slim large group payloads (the full group resource includes 30+ fields).",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups/{group-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-get",
      "options": [
        {
          "name": "group-id",
          "key": "groupId",
          "required": true,
          "description": "Azure AD group object ID. Use `list-groups` to find one."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-group --group-id 'a1b2c3d4-...' --select 'id,displayName,mail'",
      "responseShape": "single Microsoft Graph `group` resource"
    },
    {
      "name": "get-mail-attachment",
      "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.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}/attachments/{attachment-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/attachment-get",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `ask-marcel list-mail-messages`."
        },
        {
          "name": "attachment-id",
          "key": "attachmentId",
          "required": true,
          "description": "Attachment ID. Returned by `ask-marcel list-mail-attachments`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-mail-attachment --message-id 'AAMkAGI2...' --attachment-id 'AAMkABC...'",
      "responseShape": "single Microsoft Graph `attachment` resource. fileAttachments include `contentBytes` (Graph) AND `base64` (CLI mirror) so `--output-path` works; itemAttachments and referenceAttachments are returned unchanged."
    },
    {
      "name": "get-mail-message",
      "summary": "Get a single Outlook message by ID. The CLI ships a slim default `--select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,isRead,importance,bodyPreview` so an LLM caller doesn't pull a 41 KB resource just to read a subject line. Pass `--select id,subject,body` (or any other comma-separated field list) to override; for the raw RFC-822 source use `get-mail-message-mime` instead.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-get",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `ask-marcel list-mail-messages` or `list-mail-folder-messages`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-mail-message --message-id 'AAMkAGI2...'",
      "responseShape": "single Microsoft Graph `message` resource projected to the default `--select` set (or, when overridden, to the requested fields). The default omits `body`, `internetMessageHeaders`, and `uniqueBody` — request them explicitly via `--select` when you need the full HTML."
    },
    {
      "name": "get-mail-message-mime",
      "summary": "Return the raw RFC 5322 MIME source of a single Outlook message — full headers, every attachment encoded inline. Useful for archiving, full-fidelity forensic inspection, or feeding into a tool that reads MIME directly. For human-readable content prefer `get-mail-message` or `convert-mail-to-markdown`.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}/$value",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-get",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `list-mail-messages` or `search-mail-messages`."
        }
      ],
      "example": "ask-marcel get-mail-message-mime --message-id 'AAMkAD...'",
      "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."
    },
    {
      "name": "get-mail-rule",
      "summary": "Return a single Outlook message rule by ID, including its conditions and actions. Sibling to `list-mail-rules`. `--mail-folder-id` defaults to `inbox` (the only folder where rules actually live in Graph); the flag is preserved for callers that want to pass a resolved Inbox ID explicitly.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailFolders/{mail-folder-id}/messageRules/{message-rule-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/messagerule-get",
      "options": [
        {
          "name": "mail-folder-id",
          "key": "mailFolderId",
          "required": false,
          "description": "Mail folder ID or well-known name. Optional; defaults to `inbox` because that is the only folder Graph supports for message rules."
        },
        {
          "name": "message-rule-id",
          "key": "messageRuleId",
          "required": true,
          "description": "Message rule ID. Returned by `list-mail-rules`.",
          "aliases": [
            {
              "name": "rule-id",
              "key": "ruleId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-mail-rule --message-rule-id 'AQAAANC...'",
      "responseShape": "single Microsoft Graph `messageRule` resource"
    },
    {
      "name": "get-mailbox-settings",
      "summary": "Get the signed-in user's Outlook mailbox settings (timezone, working hours, automatic replies). Note: Graph silently ignores `$select` / `$expand` on this endpoint, so the CLI does NOT expose them — the full payload (including the auto-reply HTML body) is always returned. Slim client-side if you only need a subset.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailboxSettings",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-get-mailboxsettings",
      "options": [],
      "example": "ask-marcel get-mailbox-settings",
      "responseShape": "single Microsoft Graph `mailboxSettings` resource"
    },
    {
      "name": "get-my-calendar",
      "summary": "Return metadata for the signed-in user's *primary* calendar — `id`, `name`, `color`, `owner`, `canShare`, `canViewPrivateItems`, `canEdit`, `defaultOnlineMeetingProvider`. Sibling to `list-calendars` which returns every calendar (incl. shared / subscribed). Use `--select` to fetch only the fields you need.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendar",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-get-calendar",
      "options": [
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-my-calendar --select 'id,name'",
      "responseShape": "single Microsoft Graph `calendar` resource"
    },
    {
      "name": "get-my-manager",
      "summary": "Return the signed-in user's manager (a single `user` resource). When no manager is set in the directory, Graph returns 404 `Request_ResourceNotFound`; this command maps that one specific 404 to `{ ok: true, data: { manager: null, note: '...' } }` so an LLM can distinguish 'no manager' from a permission failure without parsing prose. Use `--select` to slim the response (e.g. `--select id,displayName,mail`).",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/manager",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-manager",
      "options": [
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-my-manager --select 'id,displayName,mail'",
      "responseShape": "single Microsoft Graph `user` resource on success, OR `{ manager: null, note: <string> }` when the signed-in user has no manager set. Detect the no-manager case via `data.manager === null` (also: `data.note` carries a human description)."
    },
    {
      "name": "get-my-profile-photo",
      "summary": "Download the signed-in user's profile photo (largest available size), inlined. The CLI follows the Graph 302 → CDN redirect internally so the LLM never has to fetch an external URL.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/photo/$value",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/profilephoto-get",
      "options": [],
      "example": "ask-marcel get-my-profile-photo",
      "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`."
    },
    {
      "name": "get-onenote-page-as-markdown",
      "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`.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/onenote/pages/{onenote-page-id}/content",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/page-get",
      "options": [
        {
          "name": "onenote-page-id",
          "key": "onenotePageId",
          "required": true,
          "description": "OneNote page ID. Returned by `ask-marcel list-onenote-section-pages`.",
          "aliases": [
            {
              "name": "page-id",
              "key": "pageId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-onenote-page-as-markdown --onenote-page-id '1-abc...'",
      "responseShape": "`{ contentType: \"text/markdown\", size: <chars>, text: \"...\" }` — turndown-rendered markdown of the OneNote page body."
    },
    {
      "name": "get-onenote-page-content",
      "summary": "Get the raw HTML body of a single OneNote page. Returned as a `text/html` payload so the HTML body is available verbatim (text mode prints the body raw; JSON mode wraps it in the standard `{contentType, size, text}` envelope). For markdown output use `get-onenote-page-as-markdown`.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/onenote/pages/{onenote-page-id}/content",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/page-get",
      "options": [
        {
          "name": "onenote-page-id",
          "key": "onenotePageId",
          "required": true,
          "description": "OneNote page ID. Returned by `ask-marcel list-onenote-section-pages`.",
          "aliases": [
            {
              "name": "page-id",
              "key": "pageId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-onenote-page-content --onenote-page-id '1-abc...'",
      "responseShape": "`{ contentType: \"text/html\", size: <chars>, text: \"<html>...\" }` — the rendered OneNote page body wrapped in a JSON envelope"
    },
    {
      "name": "get-organization",
      "summary": "Return the tenant's organization metadata — display name, country, verified domains, business phones, technical / security notification contacts, assigned Microsoft 365 SKUs / licensing. Graph wraps the single organization resource under `value[]` (audit v1.0.0 §D7 — even though only one tenant exists, the endpoint returns a collection). The full resource is ~57 KB; use `--select` to slim it (e.g. `--select id,displayName,verifiedDomains`).",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/organization",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/organization-list",
      "options": [
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-organization --select 'id,displayName,verifiedDomains'",
      "responseShape": "collection of one Microsoft Graph `organization` resource under `value[]`"
    },
    {
      "name": "get-planner-bucket",
      "summary": "Get the metadata of a single Microsoft Planner bucket (column / lane).",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/planner/buckets/{planner-bucket-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/plannerbucket-get",
      "options": [
        {
          "name": "planner-bucket-id",
          "key": "plannerBucketId",
          "required": true,
          "description": "Planner bucket ID. Returned by `ask-marcel list-plan-buckets`.",
          "aliases": [
            {
              "name": "bucket-id",
              "key": "bucketId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-planner-bucket --planner-bucket-id 'sFNeQRFu_kqhxpwwAhmA15gAGfoT'",
      "responseShape": "single Microsoft Graph `plannerBucket` resource"
    },
    {
      "name": "get-planner-plan",
      "summary": "Get the metadata of a single Microsoft Planner plan (title, owner group, container).",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/planner/plans/{planner-plan-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/plannerplan-get",
      "options": [
        {
          "name": "planner-plan-id",
          "key": "plannerPlanId",
          "required": true,
          "description": "Planner plan ID. Returned in the `planId` field of any task from `ask-marcel list-planner-tasks`.",
          "aliases": [
            {
              "name": "plan-id",
              "key": "planId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-planner-plan --planner-plan-id 'xqQg5FS2LkCp935s-FIFm5gAB6'",
      "responseShape": "single Microsoft Graph `plannerPlan` resource"
    },
    {
      "name": "get-planner-task",
      "summary": "Get the metadata of a single Microsoft Planner task (title, assignees, dates, completion).",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/planner/tasks/{planner-task-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/plannertask-get",
      "options": [
        {
          "name": "planner-task-id",
          "key": "plannerTaskId",
          "required": true,
          "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).",
          "aliases": [
            {
              "name": "task-id",
              "key": "taskId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-planner-task --planner-task-id '01tx7Ic7-USXEwt0lvR1cmgAH8gK'",
      "responseShape": "single Microsoft Graph `plannerTask` resource"
    },
    {
      "name": "get-planner-task-details",
      "summary": "Get the rich details (description, checklist, references) of a Microsoft Planner task.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/planner/tasks/{planner-task-id}/details",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/plannertaskdetails-get",
      "options": [
        {
          "name": "planner-task-id",
          "key": "plannerTaskId",
          "required": true,
          "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).",
          "aliases": [
            {
              "name": "task-id",
              "key": "taskId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-planner-task-details --planner-task-id '01tx7Ic7-USXEwt0lvR1cmgAH8gK'",
      "responseShape": "single Microsoft Graph `plannerTaskDetails` resource"
    },
    {
      "name": "get-shared-mailbox-message",
      "summary": "Return a single message from a shared / delegated mailbox. Use `--select` to fetch only specific fields (e.g. `--select id,subject,from,receivedDateTime`) — sibling to `get-mail-message` for /me.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/users/{user-id}/messages/{message-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-get",
      "options": [
        {
          "name": "user-id",
          "key": "userId",
          "required": true,
          "description": "Azure AD user ID or UPN of the shared mailbox."
        },
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID inside that mailbox."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-shared-mailbox-message --user-id 'shared-mailbox@contoso.com' --message-id 'AAMkAD...' --select 'id,subject,from'",
      "responseShape": "single Microsoft Graph `message` resource"
    },
    {
      "name": "get-sharepoint-list-column",
      "summary": "Return a single column definition from a SharePoint list.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/lists/{list-id}/columns/{column-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/columndefinition-get",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "list-id",
          "key": "listId",
          "required": true,
          "description": "List ID inside the site."
        },
        {
          "name": "column-id",
          "key": "columnId",
          "required": true,
          "description": "columnDefinition ID or column display name (e.g. `Title`). Returned by `list-sharepoint-list-columns`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-sharepoint-list-column --site-id '...' --list-id '...' --column-id 'Title'",
      "responseShape": "single Microsoft Graph `columnDefinition` resource"
    },
    {
      "name": "get-sharepoint-site",
      "summary": "Get the metadata of a single SharePoint site by its site ID.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/site-get",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-sharepoint-site --site-id 'contoso.sharepoint.com,1234,5678'",
      "responseShape": "single Microsoft Graph `site` resource"
    },
    {
      "name": "get-sharepoint-site-by-path",
      "summary": "Resolve a SharePoint site by its hostname + server-relative path. Use this when you have a SharePoint URL (e.g. `https://contoso.sharepoint.com/sites/Marketing`) but no site ID.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{hostname}:{path}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/site-get",
      "options": [
        {
          "name": "hostname",
          "key": "hostname",
          "required": true,
          "description": "SharePoint host, e.g. `contoso.sharepoint.com` (or `contoso-my.sharepoint.com` for personal sites). Take the host portion of your SharePoint URL."
        },
        {
          "name": "path",
          "key": "path",
          "required": true,
          "description": "`--path` (server-relative) starts with `/`, e.g. `/sites/Marketing` or `/teams/Marketing/Subsite`. Take the URL pathname after the hostname."
        }
      ],
      "example": "ask-marcel get-sharepoint-site-by-path --hostname 'contoso.sharepoint.com' --path '/sites/Marketing'",
      "responseShape": "single Microsoft Graph `site` resource"
    },
    {
      "name": "get-sharepoint-site-drive-by-id",
      "summary": "Get the metadata of a single document library (drive) on a SharePoint site by drive ID.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/drives/{drive-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-get",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
        },
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Drive (document library) ID on the site. Returned by `ask-marcel list-sharepoint-site-drives`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-sharepoint-site-drive-by-id --site-id 'contoso.sharepoint.com,1234,5678' --drive-id 'b!abcd'",
      "responseShape": "single Microsoft Graph `drive` resource"
    },
    {
      "name": "get-sharepoint-site-list",
      "summary": "Get the metadata (display name, template, columns) of a single SharePoint list.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/lists/{list-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/list-get",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
        },
        {
          "name": "list-id",
          "key": "listId",
          "required": true,
          "description": "SharePoint list ID or display name. Returned by `ask-marcel list-sharepoint-site-lists`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-sharepoint-site-list --site-id 'contoso.sharepoint.com,1234,5678' --list-id 'Documents'",
      "responseShape": "single Microsoft Graph `list` resource"
    },
    {
      "name": "get-sharepoint-site-list-item",
      "summary": "Get a single row (listItem) of a SharePoint list by ID.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/lists/{list-id}/items/{list-item-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/listitem-get",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
        },
        {
          "name": "list-id",
          "key": "listId",
          "required": true,
          "description": "SharePoint list ID or display name. Returned by `ask-marcel list-sharepoint-site-lists`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "list-item-id",
          "key": "listItemId",
          "required": true,
          "description": "listItem ID (typically a small integer). Returned by `ask-marcel list-sharepoint-site-list-items`.",
          "aliases": [
            {
              "name": "item-id",
              "key": "itemId"
            }
          ]
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-sharepoint-site-list-item --site-id 'contoso.sharepoint.com,1234,5678' --list-id 'Tasks' --list-item-id '7'",
      "responseShape": "single Microsoft Graph `listItem` resource"
    },
    {
      "name": "get-sharepoint-site-onenote-page-content",
      "summary": "Return the HTML content of a single OneNote page from a SharePoint site (parallel to `get-onenote-page-content` for `/me`). The response carries the standard `{contentType: text/html, size, text}` shape so the HTML body is available verbatim under either output format.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/onenote/pages/{onenote-page-id}/content",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/page-get",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "onenote-page-id",
          "key": "onenotePageId",
          "required": true,
          "description": "OneNote page ID inside the site.",
          "aliases": [
            {
              "name": "page-id",
              "key": "pageId"
            }
          ]
        }
      ],
      "example": "ask-marcel get-sharepoint-site-onenote-page-content --site-id 'contoso.sharepoint.com,...' --onenote-page-id 'p1'",
      "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."
    },
    {
      "name": "get-site-analytics",
      "summary": "Return view / activity analytics for a SharePoint site — `allTime` totals (visits, viewers) and `lastSevenDays` rollup. Site-level parallel to `get-drive-item-analytics`. Useful for ranking sites by attention or detecting stale workspaces. **Known empty case**: returns `{ allTime: null, lastSevenDays: null }` even on active sites when the calling identity (the Teams web client basic token) lacks the analytics scope. Do not interpret nulls as \"no activity\" — interpret as \"not available for this caller\".",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/analytics",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/itemanalytics-get",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        }
      ],
      "example": "ask-marcel get-site-analytics --site-id 'contoso.sharepoint.com,...'",
      "responseShape": "single Microsoft Graph `itemAnalytics` resource"
    },
    {
      "name": "get-specific-calendar-event",
      "summary": "Fetch a single calendar event by ID from a specific calendar. `--calendar-id primary` (or `default`) targets the signed-in user's default calendar. Use `--select` to slim large event payloads (a typical event with body+attendees runs >50 KB).",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendars/{calendar-id}/events/{event-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/event-get",
      "options": [
        {
          "name": "calendar-id",
          "key": "calendarId",
          "required": true,
          "description": "Calendar ID, or `primary` / `default` for the signed-in user's default calendar. Returned by `ask-marcel list-calendars`."
        },
        {
          "name": "event-id",
          "key": "eventId",
          "required": true,
          "description": "Event ID. Returned by `ask-marcel list-specific-calendar-events`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-specific-calendar-event --calendar-id 'primary' --event-id 'AAMkABC...' --select 'id,subject,start,end'",
      "responseShape": "single Microsoft Graph `event` resource"
    },
    {
      "name": "get-team",
      "summary": "Get the metadata of a single Microsoft Team (display name, settings, member-settings, owner group). Pass `--select displayName,description,visibility` to slim the response.",
      "category": "teams",
      "graphMethod": "GET",
      "graphPathTemplate": "/teams/{team-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/team-get",
      "options": [
        {
          "name": "team-id",
          "key": "teamId",
          "required": true,
          "description": "Microsoft Teams team ID. Returned by `ask-marcel list-joined-teams`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-team --team-id 'abc-1234-...' --select displayName,description,visibility",
      "responseShape": "single Microsoft Graph `team` resource (or projection of the requested `--select` fields)"
    },
    {
      "name": "get-team-channel",
      "summary": "Get the metadata of a single channel inside a Microsoft Team. Use `--select` to slim the response (e.g. `--select id,displayName,webUrl`) — sibling to `get-team` and `get-team-primary-channel` which both expose the same flag.",
      "category": "teams",
      "graphMethod": "GET",
      "graphPathTemplate": "/teams/{team-id}/channels/{channel-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/channel-get",
      "options": [
        {
          "name": "team-id",
          "key": "teamId",
          "required": true,
          "description": "Microsoft Teams team ID. Returned by `ask-marcel list-joined-teams`."
        },
        {
          "name": "channel-id",
          "key": "channelId",
          "required": true,
          "description": "Channel ID. Returned by `ask-marcel list-team-channels`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-team-channel --team-id 'abc-1234-...' --channel-id '19:def@thread.tacv2' --select 'id,displayName'",
      "responseShape": "single Microsoft Graph `channel` resource"
    },
    {
      "name": "get-team-primary-channel",
      "summary": "Return the team's primary (General) channel directly without having to list-then-pick. The returned `channel` has `id`, `displayName`, `webUrl`, `email` — feed `id` into `list-team-channels` siblings or `get-channel-files-folder`.",
      "category": "teams",
      "graphMethod": "GET",
      "graphPathTemplate": "/teams/{team-id}/primaryChannel",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/team-get-primarychannel",
      "options": [
        {
          "name": "team-id",
          "key": "teamId",
          "required": true,
          "description": "Microsoft Teams team ID. Returned by `list-joined-teams`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-team-primary-channel --team-id 'tm1'",
      "responseShape": "single Microsoft Graph `channel` resource"
    },
    {
      "name": "get-teams-chat-message",
      "summary": "Return a single Microsoft Teams chat message by its id via the chat substrate. Uses the chatsvcagg-audience bearer captured at login (same identity as the basic Teams token, different audience). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. Source the chat-id + message-id via `list-teams-chats-with-messages` or `list-teams-chat-messages`.",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "https://teams.microsoft.com/api/csa/{region}/api/v1/chats/{chat-id}/messages/{message-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chatmessage-get",
      "options": [
        {
          "name": "chat-id",
          "key": "chatId",
          "required": true,
          "description": "Teams chat ID. Source via `list-chats` or `list-teams-chats-with-messages`."
        },
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Teams chat message ID. Source via `list-teams-chats-with-messages` or `list-teams-chat-messages`."
        }
      ],
      "example": "ask-marcel get-teams-chat-message --chat-id '19:abc...@unq.gbl.spaces' --message-id '1700000000000'",
      "responseShape": "single Teams chat message — `id`, `from`, `imDisplayName`, `content`, `contentType`, `composeTime`, `originalArrivalTime`, etc. **Microsoft-internal schema — fields may change without notice.**",
      "stability": "experimental"
    },
    {
      "name": "get-todo-task",
      "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.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/todo/lists/{todo-task-list-id}/tasks/{todo-task-id}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/todotask-get",
      "options": [
        {
          "name": "todo-task-list-id",
          "key": "todoTaskListId",
          "required": true,
          "description": "To Do task list ID. Returned by `ask-marcel list-todo-task-lists`.",
          "aliases": [
            {
              "name": "task-list-id",
              "key": "taskListId"
            },
            {
              "name": "todo-list-id",
              "key": "todoListId"
            }
          ]
        },
        {
          "name": "todo-task-id",
          "key": "todoTaskId",
          "required": true,
          "description": "To Do task ID. Returned by `ask-marcel list-todo-tasks`. Accepts `--task-id` as a shorter alias (within this command's flag set the To Do context is unambiguous).",
          "aliases": [
            {
              "name": "task-id",
              "key": "taskId"
            }
          ]
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-todo-task --todo-task-list-id 'AAMkAGI...' --todo-task-id 'AAMkABC...'",
      "responseShape": "single Microsoft Graph `todoTask` resource (slimmed by `--select` when supplied)"
    },
    {
      "name": "get-user-manager",
      "summary": "Return a specific user's manager (a single `user` resource). When the user has no manager set in the directory, Graph returns 404 `Request_ResourceNotFound`; this command maps that one specific 404 to `{ ok: true, data: { manager: null, note: '...' } }` (same shape as `get-my-manager`) so an LLM can distinguish 'no manager' from 'unknown user' with a single discriminator across both commands. Use `--select` to slim the response.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/users/{user-id}/manager",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-manager",
      "options": [
        {
          "name": "user-id",
          "key": "userId",
          "required": true,
          "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)."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel get-user-manager --user-id 'alice@contoso.com' --select 'id,displayName,mail'",
      "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`)."
    },
    {
      "name": "list-all-onenote-sections",
      "summary": "List every OneNote section the signed-in user can see, across all notebooks.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/onenote/sections",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/onenote-list-sections",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-all-onenote-sections",
      "responseShape": "collection of Microsoft Graph `onenoteSection` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-calendar-event-instances",
      "summary": "List the individual occurrences of a recurring calendar event over a date range. Both ISO date-time params are required by Graph. `--calendar-id` is optional and defaults to `primary` (the signed-in user’s default calendar) — most callers know the event-id but not which calendar it lives in. Pass an explicit `--calendar-id` only when targeting a non-default calendar.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendars/{calendar-id}/events/{event-id}/instances?startDateTime={start-date-time}&endDateTime={end-date-time}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/event-list-instances",
      "options": [
        {
          "name": "calendar-id",
          "key": "calendarId",
          "required": false,
          "description": "Calendar ID, or `primary` / `default` for the signed-in user’s default calendar. Optional; defaults to `primary`. Returned by `ask-marcel list-calendars`.",
          "argumentHint": {
            "kind": "magicValue",
            "values": [
              "primary",
              "default"
            ]
          }
        },
        {
          "name": "event-id",
          "key": "eventId",
          "required": true,
          "description": "Recurring event ID. Returned by `ask-marcel list-specific-calendar-events`."
        },
        {
          "name": "start-date-time",
          "key": "startDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "end-date-time",
          "key": "endDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-calendar-event-instances --calendar-id 'AAMkAGI2THVS...' --event-id 'AAMkABC...' --start-date-time '2026-04-01T00:00:00Z' --end-date-time '2026-05-01T00:00:00Z'",
      "responseShape": "collection of Microsoft Graph `event` resources (single occurrences) under `value[]`",
      "pagination": true
    },
    {
      "name": "list-calendar-events",
      "summary": "List the events in the signed-in user’s default calendar (does not expand recurrences).",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/events",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-events",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-calendar-events",
      "responseShape": "collection of Microsoft Graph `event` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-calendar-events-delta",
      "summary": "Get the incremental change set (added / modified / deleted events) for the signed-in user's default calendar. Use the `@odata.deltaLink` from a previous response to resume. The CLI translates `--top` into the `Prefer: odata.maxpagesize=N` header internally; `$top` as a URL query is rejected by Graph (`ErrorInvalidUrlQuery`). Other OData passthroughs (`$select`, `$filter`, `$orderby`, `$skip`) are silently ignored by Graph on this delta endpoint, so the CLI does NOT expose them — slice / sort / project client-side. Most tenants accept the call without `--top` and return a sane page (~200 events); pass `--top` only when you want a smaller bound. If Graph returns an empty `UnknownError:` (rare), the CLI rewrites it to a hint pointing at the `--top` workaround.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/events/delta()",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/event-delta",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        }
      ],
      "example": "ask-marcel list-calendar-events-delta --top 50",
      "responseShape": "collection of changed Microsoft Graph `event` resources under `data.value[]`. Cursor tokens are hoisted to envelope level: top-level `nextLink` while paging, then top-level `deltaLink` on the final page.",
      "pagination": true
    },
    {
      "name": "list-calendar-group-calendars",
      "summary": "List the calendars inside one calendar group.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendarGroups/{calendar-group-id}/calendars",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/calendargroup-list-calendars",
      "options": [
        {
          "name": "calendar-group-id",
          "key": "calendarGroupId",
          "required": true,
          "description": "Calendar group ID. Returned by `list-calendar-groups`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-calendar-group-calendars --calendar-group-id 'AAMkADk0...'",
      "responseShape": "collection of Microsoft Graph `calendar` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-calendar-groups",
      "summary": "List the signed-in user's calendar groups — Outlook's organizational layer above individual calendars (e.g. \"My Calendars\", \"Other Calendars\", \"Birthdays\"). Use the returned `id` with `list-calendar-group-calendars` to drill in.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendarGroups",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-calendargroups",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-calendar-groups",
      "responseShape": "collection of Microsoft Graph `calendarGroup` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-calendar-view",
      "summary": "List the signed-in user's default-calendar events with recurrence expanded into individual occurrences in a date range. Both date-time params accept strict ISO 8601 (`2026-04-01T00:00:00Z`) AND the CLI's relative shapes (`7d`, `today`, `monday`, `start-of-month`, …) so a question like \"what's on my calendar this week\" no longer requires the LLM to compute timestamps by hand.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-calendarview",
      "options": [
        {
          "name": "start-date-time",
          "key": "startDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "end-date-time",
          "key": "endDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-calendar-view --start-date-time 'start-of-week' --end-date-time 'end-of-week'",
      "responseShape": "collection of Microsoft Graph `event` resources (single occurrences) under `value[]`"
    },
    {
      "name": "list-calendar-view-delta",
      "summary": "Get the first page of the incremental change set of expanded calendar-view occurrences over a date range. Subsequent pages: feed the returned `@odata.nextLink` to `next-page`; resume later via the `@odata.deltaLink`. The CLI translates `--top` into the `Prefer: odata.maxpagesize=N` header internally — `$top` as a URL query is rejected by Graph (`ErrorInvalidUrlQuery`). Other OData passthroughs (`$select`, `$filter`, `$orderby`, `$skip`) are silently ignored by Graph on this delta endpoint, so the CLI does NOT expose them.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendarView/delta()?startDateTime={start-date-time}&endDateTime={end-date-time}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/event-delta",
      "options": [
        {
          "name": "start-date-time",
          "key": "startDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "end-date-time",
          "key": "endDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        }
      ],
      "example": "ask-marcel list-calendar-view-delta --start-date-time '2026-04-01T00:00:00Z' --end-date-time '2026-05-01T00:00:00Z' --top 50",
      "responseShape": "collection of changed Microsoft Graph `event` occurrences under `data.value[]`. Cursor tokens are hoisted to envelope level: top-level `nextLink` while paging, then top-level `deltaLink` on the final page.",
      "pagination": true
    },
    {
      "name": "list-calendars",
      "summary": "List the calendars in the signed-in user’s mailbox (default + secondary calendars + shared calendars).",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendars",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-calendars",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-calendars",
      "responseShape": "collection of Microsoft Graph `calendar` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-chat-members",
      "summary": "List the members of a single Microsoft Teams chat. Graph rejects `$top` / `$orderby` / `$expand` on this endpoint, so the CLI advertises only the subset Graph honours (`--skip`, `--select`, `--filter`).",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "/chats/{chat-id}/members",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chat-list-members",
      "options": [
        {
          "name": "chat-id",
          "key": "chatId",
          "required": true,
          "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`."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        }
      ],
      "example": "ask-marcel list-chat-members --chat-id '19:abc...@thread.v2'",
      "responseShape": "collection of Microsoft Graph `conversationMember` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-chats",
      "summary": "List the signed-in user's Microsoft Teams chats (1:1, group, and meeting chats). The CLI ships a slim default `--select=id,topic,chatType,createdDateTime,lastUpdatedDateTime`; pass `--select id,topic,webUrl,...` to widen. Returns chat metadata only — reading chat *messages* needs `Chat.Read*` which neither token grants. Requires the M365ChatClient elevated token captured at login (the basic Teams web client token lacks `Chat.ReadBasic`). Graph rejects `$orderby` and hangs on `$expand` for this endpoint, so the CLI advertises only the subset Graph honours (`--top`, `--skip`, `--select`, `--filter`).",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/chats",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chat-list",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        }
      ],
      "example": "ask-marcel list-chats",
      "responseShape": "collection of Microsoft Graph `chat` resources under `value[]`, each projected to the default `--select` set (or, when overridden, to the requested fields).",
      "pagination": true
    },
    {
      "name": "list-conversation-messages",
      "summary": "List every message in a single Outlook conversation (thread) using `$filter=conversationId eq '...'`. Reconstructs a complete thread regardless of which subject lines or folders the replies landed in. Accepts the OData passthrough flags top/skip/select/expand — the filter and orderby passthroughs are intentionally omitted (the path already pins a `$filter`, and Graph rejects this filter combined with `$orderby` as `InefficientFilter` since `conversationId` is not a sortable index). The caller can sort by `receivedDateTime` client-side. KQL `$search` does not index `conversationId`, so `$filter` is the only documented Graph idiom for whole-thread retrieval.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages?$filter=conversationId eq '{conversation-id}'",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-messages",
      "options": [
        {
          "name": "conversation-id",
          "key": "conversationId",
          "required": true,
          "description": "Outlook `conversationId` of any message in the thread (returned by every mail-listing command and by `get-mail-message`)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-conversation-messages --conversation-id 'AAQkAD...=' --top 5 --select id,subject,receivedDateTime",
      "responseShape": "collection of Microsoft Graph `message` resources under `value[]` (unordered)",
      "pagination": true
    },
    {
      "name": "list-drive-item-permissions",
      "summary": "List the sharing permissions on a OneDrive / SharePoint file or folder.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/permissions",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-list-permissions",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the file or folder. Returned by `list-folder-files`, `search-onedrive-files`, or `get-drive-item`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-drive-item-permissions --drive-id 'b!1234' --item-id '01ABC'",
      "responseShape": "collection of Microsoft Graph `permission` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-drive-item-thumbnails",
      "summary": "List thumbnail URLs (small / medium / large) for a OneDrive / SharePoint file. Each thumbnail set has pre-signed CDN URLs you can render in a UI without further auth.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/thumbnails",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-list-thumbnails",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID. Returned by `list-folder-files` or `search-onedrive-files`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-drive-item-thumbnails --drive-id 'b!1234' --item-id '01ABC'",
      "responseShape": "collection of Microsoft Graph `thumbnailSet` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-drive-item-versions",
      "summary": "List the historical versions of a OneDrive / SharePoint file (each save creates a new version). Note: each version's `id` is a stringified float like `\"79.0\"` (NOT an integer like `79`) — pass it literally to the `download-drive-item-version` command (it accepts an `original | pdf | markdown` format selector); numeric coercion silently fails because Graph rejects `79` against a path templated for stringified floats.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/versions",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-list-versions",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "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."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the file. Returned by `list-folder-files` or `search-onedrive-files`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-drive-item-versions --drive-id 'b!1234' --item-id '01ABC'",
      "responseShape": "collection of Microsoft Graph `driveItemVersion` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-drives",
      "summary": "List all OneDrive / SharePoint drives the signed-in user has access to. On personal accounts this returns only the user's primary OneDrive (single entry in `value[]`); on tenanted accounts it includes every drive the user can reach including delegated mailboxes and shared SharePoint document libraries.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/drives",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-list",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-drives",
      "responseShape": "collection of Microsoft Graph `drive` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-excel-comments",
      "summary": "List the modern threaded comments anchored to cells in an Excel workbook (the New Comments feature, distinct from legacy notes). Each `workbookComment` has `content`, `contentType`, `task` state, plus replies via the comment's `replies` navigation.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/comments",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/workbook-list-comments",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-excel-comments --drive-id 'b!1234' --item-id '01ABC'",
      "responseShape": "collection of Microsoft Graph `workbookComment` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-excel-defined-names",
      "summary": "List the workbook's defined names (named ranges, named formulas, named constants). Each `workbookNamedItem` has `name`, `value` (the formula or address), `comment`, and `scope` (workbook or worksheet). Useful for understanding workbook structure before reading ranges.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/names",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/workbook-list-names",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-excel-defined-names --drive-id 'b!1234' --item-id '01ABC'",
      "responseShape": "collection of Microsoft Graph `workbookNamedItem` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-excel-table-rows",
      "summary": "List the data rows of a named Excel table (excluding the header row). Note: Graph silently ignores `$filter` and `$orderby` on this endpoint, so the CLI does not expose those flags — slice / sort client-side.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/tables/{table-id}/rows",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/workbooktable-list-rows",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "table-id",
          "key": "tableId",
          "required": true,
          "description": "Workbook table ID or table name. Returned by `ask-marcel list-excel-tables`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-excel-table-rows --drive-id 'b!1234' --item-id '01XLSX' --table-id 'Table1'",
      "responseShape": "collection of Microsoft Graph `workbookTableRow` resources (each `values` is a 2D array) under `value[]`",
      "pagination": true
    },
    {
      "name": "list-excel-tables",
      "summary": "List the named tables across every worksheet in an Excel workbook. Note: Graph silently ignores `$filter` and `$orderby` on this endpoint, so the CLI does not expose those flags — slice / sort client-side.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/tables",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/workbook-list-tables",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file. Returned by `list-folder-files` or `search-onedrive-files`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-excel-tables --drive-id 'b!1234' --item-id '01XLSX'",
      "responseShape": "collection of Microsoft Graph `workbookTable` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-excel-worksheet-charts",
      "summary": "List the charts on a worksheet. Each `workbookChart` has `id`, `name`, `height`, `width`, `top`, `left`. Use the chart's image endpoint (`.../charts/{id}/image()`) to render the chart as a base64 PNG.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/charts",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/worksheet-list-charts",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "worksheet-id",
          "key": "worksheetId",
          "required": true,
          "description": "Accepts either the worksheet display name (e.g. `Sheet1`, `PROJECT`) or the worksheet `id` GUID returned by `list-excel-worksheets`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-excel-worksheet-charts --drive-id 'b!1234' --item-id '01ABC' --worksheet-id 'Sheet1'",
      "responseShape": "collection of Microsoft Graph `workbookChart` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-excel-worksheet-pivot-tables",
      "summary": "List the pivot tables on a worksheet. Each `workbookPivotTable` has `name` and a navigation to its source `workbookWorksheet`. Useful for understanding analytical structure inside a workbook.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/worksheets/{worksheet-id}/pivotTables",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/worksheet-list-pivottables",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "OneDrive / SharePoint drive ID."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file."
        },
        {
          "name": "worksheet-id",
          "key": "worksheetId",
          "required": true,
          "description": "Accepts either the worksheet display name (e.g. `Sheet1`, `PROJECT`) or the worksheet `id` GUID returned by `list-excel-worksheets`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-excel-worksheet-pivot-tables --drive-id 'b!1234' --item-id '01ABC' --worksheet-id 'Sheet1'",
      "responseShape": "collection of Microsoft Graph `workbookPivotTable` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-excel-worksheets",
      "summary": "List the worksheets (tabs) inside an Excel workbook stored in OneDrive / SharePoint. Returns a clear \"not an accessible Excel workbook\" error if the item is a folder, non-.xlsx file, or sensitivity-label-blocked. Note: Graph silently ignores `$top`, `$filter`, and `$orderby` on this endpoint, so the CLI does not expose those flags — slice / sort client-side.",
      "category": "excel",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/workbook/worksheets",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/workbook-list-worksheets",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID containing the workbook. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the .xlsx file. Returned by `list-folder-files` or `search-onedrive-files`."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-excel-worksheets --drive-id 'b!1234' --item-id '01XLSX'",
      "responseShape": "collection of Microsoft Graph `worksheet` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-focused-inbox-overrides",
      "summary": "List the signed-in user's Focused Inbox classification overrides — sender addresses they've manually moved to Focused or Other, which override Microsoft's automatic classifier.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/inferenceClassification/overrides",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/inferenceclassification-list-overrides",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-focused-inbox-overrides",
      "responseShape": "collection of Microsoft Graph `inferenceClassificationOverride` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-folder-files",
      "summary": "List the children (files and subfolders) of a folder in OneDrive / SharePoint.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/items/{item-id}/children",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-list-children",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "item-id",
          "key": "itemId",
          "required": true,
          "description": "driveItem ID of the folder (Graph identifies folders as driveItems too — there is no separate folder type). Use the root folder ID from `ask-marcel get-drive-root-item` to list the top of a drive. Accepts `--folder-id` as an alias since the command name implies \"folder\".",
          "aliases": [
            {
              "name": "folder-id",
              "key": "folderId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-folder-files --drive-id 'b!1234' --item-id '01ROOT'",
      "responseShape": "collection of Microsoft Graph `driveItem` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-followed-drive-items",
      "summary": "List driveItems the signed-in user has explicitly followed (the OneDrive star). A small, hand-curated set of frequently-revisited files, distinct from the algorithmic `list-recent-files` and `list-recently-used-insights`.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/drive/following",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-list-following",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-followed-drive-items",
      "responseShape": "collection of Microsoft Graph `driveItem` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-group-calendar-view",
      "summary": "Return a date-windowed calendar view from a unified (Microsoft 365) group's calendar. Recurring events are expanded into individual occurrences across the window. Only Microsoft 365 groups have a calendar — security and distribution groups return `MailboxNotEnabledForRESTAPI`.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups/{group-id}/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-list-calendarview",
      "options": [
        {
          "name": "group-id",
          "key": "groupId",
          "required": true,
          "description": "Azure AD group object ID for a unified (Microsoft 365) group."
        },
        {
          "name": "start-date-time",
          "key": "startDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "end-date-time",
          "key": "endDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-group-calendar-view --group-id 'a1b2c3d4-...' --start-date-time '2026-04-01T00:00:00Z' --end-date-time '2026-05-01T00:00:00Z'",
      "responseShape": "collection of Microsoft Graph `event` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-group-conversations",
      "summary": "List conversations in a unified (Microsoft 365) group inbox. Each conversation aggregates one or more threads. Only Microsoft 365 groups have a mailbox — security and distribution groups return `MailboxNotEnabledForRESTAPI`. Verify the group is unified before calling.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups/{group-id}/conversations",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-list-conversations",
      "options": [
        {
          "name": "group-id",
          "key": "groupId",
          "required": true,
          "description": "Azure AD group object ID for a unified (Microsoft 365) group."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-group-conversations --group-id 'a1b2c3d4-...'",
      "responseShape": "collection of Microsoft Graph `conversation` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-group-events",
      "summary": "List events from a unified (Microsoft 365) group's calendar. Only Microsoft 365 groups have a calendar — security and distribution groups return an empty `value[]` or 404.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups/{group-id}/events",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-list-events",
      "options": [
        {
          "name": "group-id",
          "key": "groupId",
          "required": true,
          "description": "Azure AD group object ID for a unified (Microsoft 365) group. Use `list-groups` to find one."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-group-events --group-id 'a1b2c3d4-...'",
      "responseShape": "collection of Microsoft Graph `event` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-group-members",
      "summary": "List members of an Azure AD / Microsoft 365 group. Returns users, groups, and other directoryObjects depending on the group's membership.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups/{group-id}/members",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-list-members",
      "options": [
        {
          "name": "group-id",
          "key": "groupId",
          "required": true,
          "description": "Azure AD group object ID. Use `list-groups` to find one."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-group-members --group-id 'a1b2c3d4-...'",
      "responseShape": "collection of Microsoft Graph `directoryObject` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-group-owners",
      "summary": "List the owners of an Azure AD / Microsoft 365 group.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups/{group-id}/owners",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-list-owners",
      "options": [
        {
          "name": "group-id",
          "key": "groupId",
          "required": true,
          "description": "Azure AD group object ID. Use `list-groups` to find one."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-group-owners --group-id 'a1b2c3d4-...'",
      "responseShape": "collection of Microsoft Graph `directoryObject` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-group-threads",
      "summary": "List threads in a unified (Microsoft 365) group inbox. Threads are flatter than conversations — one per topic, useful when conversation-level grouping isn't needed. Only Microsoft 365 groups have a mailbox — security and distribution groups return `MailboxNotEnabledForRESTAPI`.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups/{group-id}/threads",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-list-threads",
      "options": [
        {
          "name": "group-id",
          "key": "groupId",
          "required": true,
          "description": "Azure AD group object ID for a unified (Microsoft 365) group."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-group-threads --group-id 'a1b2c3d4-...'",
      "responseShape": "collection of Microsoft Graph `conversationThread` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-groups",
      "summary": "List Microsoft 365 groups, security groups, and distribution groups in the tenant directory. Use `--top` and `next-page` to paginate over very large directories.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/groups",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/group-list",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-groups",
      "responseShape": "collection of Microsoft Graph `group` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-incomplete-planner-tasks",
      "summary": "List every incomplete Microsoft Planner task assigned to or owned by the signed-in user, across every plan. Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the completion-percent predicate, and Graph rejects two `$filter` query params. If you supply `--filter` anyway, the CLI returns a clear pointer to `list-planner-tasks`.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/planner/tasks?$filter=percentComplete ne 100",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/planneruser-list-tasks",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-incomplete-planner-tasks --top 25",
      "responseShape": "collection of Microsoft Graph `plannerTask` resources under `value[]` where `percentComplete < 100`",
      "pagination": true
    },
    {
      "name": "list-incomplete-todo-tasks",
      "summary": "List every incomplete Microsoft To Do task in a given list (status not equal to `completed`). Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the completion-status predicate, and Graph rejects two `$filter` query params. If you supply `--filter` anyway, the CLI returns a clear pointer to `list-todo-tasks` (which lets you AND your predicate with the completion filter yourself).",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/todo/lists/{todo-task-list-id}/tasks?$filter=status ne 'completed'",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/todotasklist-list-tasks",
      "options": [
        {
          "name": "todo-task-list-id",
          "key": "todoTaskListId",
          "required": true,
          "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.",
          "aliases": [
            {
              "name": "task-list-id",
              "key": "taskListId"
            },
            {
              "name": "todo-list-id",
              "key": "todoListId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-incomplete-todo-tasks --todo-task-list-id 'tasks' --top 5",
      "responseShape": "collection of Microsoft Graph `todoTask` resources under `value[]` where `status != \"completed\"`",
      "pagination": true
    },
    {
      "name": "list-joined-teams",
      "summary": "List the Microsoft Teams the signed-in user is a member of. Note: this endpoint does NOT accept the standard OData query parameters — Graph rejects `$top`/`$select`/`$filter`/etc. on `/me/joinedTeams` with `Query option 'X' is not allowed`. The CLI omits the OData passthrough on this command for that reason; pass post-processing through `jq` instead if you need to slice the response.",
      "category": "teams",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/joinedTeams",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-joinedteams",
      "options": [],
      "example": "ask-marcel list-joined-teams",
      "responseShape": "collection of Microsoft Graph `team` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-mail-attachments",
      "summary": "List the attachments (file, item, reference) on a single Outlook message. The CLI ships an opinionated default `--select=id,name,contentType,size,isInline` so an LLM that doesn't slim the response itself doesn't accidentally pull multi-MB `contentBytes` for every attachment (a single 1.5 MB image attachment would otherwise blow the context window). The `@odata.type` discriminator is always returned by Graph regardless of `$select` (and Graph rejects asking for it explicitly). To fetch the actual bytes, call `get-mail-attachment` for the one you need (or override `--select` if you really want the raw inline payload).",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages/{message-id}/attachments",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-list-attachments",
      "options": [
        {
          "name": "message-id",
          "key": "messageId",
          "required": true,
          "description": "Outlook message ID. Returned by `ask-marcel list-mail-messages` or `list-mail-folder-messages`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-mail-attachments --message-id 'AAMkAGI2...'",
      "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` — these fields are the discriminator the attachment-converting commands branch on; don't be surprised to see them appear even if you didn't request them.",
      "pagination": true
    },
    {
      "name": "list-mail-child-folders",
      "summary": "List the subfolders of a single Outlook mail folder (e.g. subfolders of Inbox).",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailFolders/{mail-folder-id}/childFolders",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/mailfolder-list-childfolders",
      "options": [
        {
          "name": "mail-folder-id",
          "key": "mailFolderId",
          "required": true,
          "description": "mailFolder ID. Returned by `ask-marcel list-mail-folders`. Well-known names also work, e.g. `inbox`, `sentitems`, `drafts`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-mail-child-folders --mail-folder-id 'inbox'",
      "responseShape": "collection of Microsoft Graph `mailFolder` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-mail-folder-messages",
      "summary": "List the messages inside a specific Outlook mail folder (Inbox, custom folder, etc.).",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailFolders/{mail-folder-id}/messages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/mailfolder-list-messages",
      "options": [
        {
          "name": "mail-folder-id",
          "key": "mailFolderId",
          "required": true,
          "description": "mailFolder ID. Returned by `ask-marcel list-mail-folders`. Well-known names also work, e.g. `inbox`, `sentitems`, `drafts`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-mail-folder-messages --mail-folder-id 'inbox'",
      "responseShape": "collection of Microsoft Graph `message` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-mail-folder-messages-delta",
      "summary": "Track incremental changes (added / updated / deleted messages) within a single mail folder using Microsoft Graph delta tokens. The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed since.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailFolders/{mail-folder-id}/messages/delta()",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-delta",
      "options": [
        {
          "name": "mail-folder-id",
          "key": "mailFolderId",
          "required": true,
          "description": "Mail folder ID or well-known name (`inbox`, `archive`, `sentitems`, `deleteditems`, `junkemail`, `drafts`). Returned by `list-mail-folders`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-mail-folder-messages-delta --mail-folder-id 'inbox'",
      "responseShape": "collection of Microsoft Graph `message` resources under `data.value[]`. Cursor tokens are hoisted to envelope level: top-level `nextLink` while paging, then top-level `deltaLink` on the final page (CLI strips the original `@odata.*` keys from `data`).",
      "pagination": true
    },
    {
      "name": "list-mail-folders",
      "summary": "List the top-level mail folders in the signed-in user’s Outlook mailbox (Inbox, Sent Items, etc.).",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailFolders",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-mailfolders",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-mail-folders",
      "responseShape": "collection of Microsoft Graph `mailFolder` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-mail-folders-delta",
      "summary": "Track incremental changes to the mail-folder tree itself (folders added / renamed / deleted). The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed. Companion to `list-mail-folder-messages-delta` which tracks message changes inside one folder. Note: Graph explicitly rejects `$top`, `$filter`, `$orderby`, and `$search` on this delta endpoint (`ErrorInvalidUrlQuery: not supported with change tracking over the 'Folders' resource`), so the OData passthrough is intentionally NOT exposed here.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailFolders/delta()",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/mailfolder-delta",
      "options": [],
      "example": "ask-marcel list-mail-folders-delta",
      "responseShape": "collection of Microsoft Graph `mailFolder` resources under `data.value[]`. Cursor tokens are hoisted to envelope level: top-level `nextLink` while paging, then top-level `deltaLink` on the final page.",
      "pagination": true
    },
    {
      "name": "list-mail-messages",
      "summary": "List the most recent messages from across the signed-in user's entire Outlook mailbox (every folder including Sent, Archive, Junk; default sort `receivedDateTime` desc). The CLI ships a slim default `--select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,isRead,importance,bodyPreview` so a page of 25 messages stays ~30-60 KB instead of ~1 MB. Pass `--select id,subject,body` (or any other comma-separated field list) to override. Use `list-mail-folder-messages` to scope to a single folder such as Inbox.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-messages",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-mail-messages",
      "responseShape": "collection of Microsoft Graph `message` resources under `value[]`, each projected to the default `--select` set (or the requested fields when overridden). The default omits `body`, `internetMessageHeaders`, and `uniqueBody`.",
      "pagination": true
    },
    {
      "name": "list-mail-rules",
      "summary": "List the message rules on the Outlook Inbox. Microsoft Graph only supports message rules on the Inbox folder; passing any other folder ID (drafts, sentitems, archive, a custom folder) returns `MailFolderNotSupportedError` from Graph. `--mail-folder-id` defaults to `inbox` because that is the only value Graph accepts; the flag is kept (optional) for callers that want to pass a resolved Inbox ID explicitly. Note: Graph silently ignores every OData passthrough on this endpoint, so the CLI does NOT expose them — the full rule set is always returned.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/mailFolders/{mail-folder-id}/messageRules",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/mailfolder-list-messagerules",
      "options": [
        {
          "name": "mail-folder-id",
          "key": "mailFolderId",
          "required": false,
          "description": "mailFolder ID. Optional; defaults to `inbox`. In practice only `inbox` (or its resolved ID) works — Graph rejects every other folder for messageRules with `MailFolderNotSupportedError`."
        }
      ],
      "example": "ask-marcel list-mail-rules",
      "responseShape": "collection of Microsoft Graph `messageRule` resources under `value[]`"
    },
    {
      "name": "list-my-direct-reports",
      "summary": "List the signed-in user's direct reports (employees who report to them in the directory). When `--orderby` is supplied the CLI auto-injects the `ConsistencyLevel: eventual` header Graph requires on directory endpoints — otherwise Graph rejects the sort with `Request_UnsupportedQuery`.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/directReports",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-directreports",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-my-direct-reports",
      "responseShape": "collection of Microsoft Graph `directoryObject` resources (typically `user`) under `value[]`",
      "pagination": true
    },
    {
      "name": "list-my-memberships",
      "summary": "List the groups, directory roles, and administrative units the signed-in user is a member of. Each entry's `@odata.type` distinguishes #microsoft.graph.group from #microsoft.graph.directoryRole, etc.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/memberOf",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-memberof",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-my-memberships",
      "responseShape": "collection of Microsoft Graph `directoryObject` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-my-transitive-memberships",
      "summary": "List all groups, directory roles, and administrative units the signed-in user is a member of *transitively* — including memberships inherited via nested groups. Sibling to `list-my-memberships` (`/me/memberOf`) which only returns direct memberships.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/transitiveMemberOf",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-transitivememberof",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-my-transitive-memberships",
      "responseShape": "collection of Microsoft Graph `directoryObject` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-onenote-notebook-sections",
      "summary": "List the top-level sections of a single OneNote notebook (flat — does NOT recurse into section groups; use `list-all-onenote-sections` to flatten every notebook the user has access to).",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/onenote/notebooks/{notebook-id}/sections",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/notebook-list-sections",
      "options": [
        {
          "name": "notebook-id",
          "key": "notebookId",
          "required": true,
          "description": "OneNote notebook ID. Returned by `ask-marcel list-onenote-notebooks`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-onenote-notebook-sections --notebook-id '1-12abc...'",
      "responseShape": "collection of Microsoft Graph `onenoteSection` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-onenote-notebooks",
      "summary": "List the OneNote notebooks the signed-in user owns or has access to (sorted by `createdDateTime` desc by Graph; soft-deleted notebooks excluded).",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/onenote/notebooks",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/onenote-list-notebooks",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-onenote-notebooks",
      "responseShape": "collection of Microsoft Graph `notebook` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-onenote-section-pages",
      "summary": "List the pages inside a single OneNote section.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/onenote/sections/{onenote-section-id}/pages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/section-list-pages",
      "options": [
        {
          "name": "onenote-section-id",
          "key": "onenoteSectionId",
          "required": true,
          "description": "OneNote section ID. Returned by `ask-marcel list-onenote-notebook-sections` or `list-all-onenote-sections`.",
          "aliases": [
            {
              "name": "section-id",
              "key": "sectionId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-onenote-section-pages --onenote-section-id '1-abc...'",
      "responseShape": "collection of Microsoft Graph `onenotePage` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-outlook-categories",
      "summary": "List the signed-in user's Outlook color categories — the named tags that can be applied to mail, calendar items, and contacts. Each entry has `displayName` and a `color` from Outlook's preset palette. Note: Graph silently ignores every OData passthrough on this endpoint (`$top`, `$skip`, `$select`, `$filter`, `$orderby`, `$expand`), so the CLI does not expose any of those flags — the full collection is always returned. Slice client-side.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/outlook/masterCategories",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/outlookuser-list-mastercategories",
      "options": [],
      "example": "ask-marcel list-outlook-categories",
      "responseShape": "collection of Microsoft Graph `outlookCategory` resources under `value[]`"
    },
    {
      "name": "list-plan-buckets",
      "summary": "List the buckets (columns / lanes) of a Microsoft Planner plan. Note: Graph silently drops `$top`, `$skip`, `$filter`, and `$orderby` on this endpoint, so the CLI advertises only `--select` — slice / sort client-side.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/planner/plans/{planner-plan-id}/buckets",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/plannerplan-list-buckets",
      "options": [
        {
          "name": "planner-plan-id",
          "key": "plannerPlanId",
          "required": true,
          "description": "Planner plan ID. Returned in the `planId` field of any task from `ask-marcel list-planner-tasks`.",
          "aliases": [
            {
              "name": "plan-id",
              "key": "planId"
            }
          ]
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        }
      ],
      "example": "ask-marcel list-plan-buckets --planner-plan-id 'xqQg5FS2LkCp935s-FIFm5gAB6'",
      "responseShape": "collection of Microsoft Graph `plannerBucket` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-plan-tasks",
      "summary": "List every task within a Microsoft Planner plan, regardless of completion status (Graph orders by `orderHint`). Use `list-incomplete-planner-tasks` for the across-plans incomplete view. Note: Graph silently ignores standard OData query parameters on `/planner/plans/{id}/tasks` (`$top` returns the full set anyway), so the OData passthrough is intentionally NOT exposed — pipe the response through `jq` to slice client-side.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/planner/plans/{planner-plan-id}/tasks",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/plannerplan-list-tasks",
      "options": [
        {
          "name": "planner-plan-id",
          "key": "plannerPlanId",
          "required": true,
          "description": "Planner plan ID. Returned in the `planId` field of any task from `ask-marcel list-planner-tasks`.",
          "aliases": [
            {
              "name": "plan-id",
              "key": "planId"
            }
          ]
        }
      ],
      "example": "ask-marcel list-plan-tasks --planner-plan-id 'xqQg5FS2LkCp935s-FIFm5gAB6'",
      "responseShape": "collection of Microsoft Graph `plannerTask` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-planner-plans",
      "summary": "List every Microsoft Planner plan the signed-in user has access to (across every group). Use this to discover plan IDs without needing an existing task as the entry point. Note: Graph silently drops `$top`, `$skip`, `$filter`, and `$orderby` on this endpoint, so the CLI advertises only `--select` — slice / sort client-side.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/planner/plans",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/planneruser-list-plans",
      "options": [
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        }
      ],
      "example": "ask-marcel list-planner-plans",
      "responseShape": "collection of Microsoft Graph `plannerPlan` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-planner-tasks",
      "summary": "List every Microsoft Planner task assigned to or owned by the signed-in user, across all plans.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/planner/tasks",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/planneruser-list-tasks",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-planner-tasks",
      "responseShape": "collection of Microsoft Graph `plannerTask` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-recent-files",
      "summary": "List the signed-in user's most recently used / opened OneDrive and SharePoint files, ranked by Microsoft's recency signal. The strongest single answer to \"what is this user working on right now?\". Note: Graph's recent-files feed is signal-driven and can lag the underlying drive by 24-48 hours — `lastModifiedDateTime` here may be older than the file's true mtime. For \"what is the actual latest version?\" call `list-drive-item-versions` on a specific item.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/drive/recent",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-recent",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-recent-files",
      "responseShape": "collection of Microsoft Graph `driveItem` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-recently-used-insights",
      "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`.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/insights/used",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/insights-list-used",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-recently-used-insights",
      "responseShape": "collection of Microsoft Graph `usedInsight` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-relevant-people",
      "summary": "List people relevant to the signed-in user — colleagues they email and meet with most. Microsoft's relevance ranking, not the full directory. Returns `displayName`, `emailAddresses`, `jobTitle`, `companyName`, etc.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/people",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-people",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-relevant-people",
      "responseShape": "collection of Microsoft Graph `person` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-room-lists",
      "summary": "List room lists — usually one per building. Use these to scope a room search by location: a roomList groups the rooms in one office, then `/places/{roomList}/rooms` lists just those rooms. Pass `--top N` to limit the response on large tenants.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/places/microsoft.graph.roomList",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/place-list",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-room-lists",
      "responseShape": "collection of Microsoft Graph `roomList` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-rooms",
      "summary": "List bookable meeting rooms in the tenant. Each `room` has `displayName`, `emailAddress`, `capacity`, `building`, `floorNumber`, and `isWheelChairAccessible`. Use the `emailAddress` as a meeting `attendee` for room booking. Pass `--top 5` to limit the response — large tenants return tens of KB by default.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/places/microsoft.graph.room",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/place-list",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-rooms",
      "responseShape": "collection of Microsoft Graph `room` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sensitivity-labels",
      "summary": "List the Microsoft Information Protection sensitivity labels available to the signed-in user — the labels Outlook / Word / SharePoint surfaces in the \"Sensitivity\" picker (e.g. Public / Internal / Confidential / Highly Confidential). Each label has `id`, `displayName`, `priority`, `isAppliable`, `tooltip`.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/informationProtection/sensitivityLabels",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/security-informationprotection-list-sensitivitylabels",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sensitivity-labels",
      "responseShape": "collection of Microsoft Graph `sensitivityLabel` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-shared-calendar-events",
      "summary": "List events from another user's primary calendar (shared / delegated access). 403 without `Calendars.Read.Shared`.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/users/{user-id}/calendar/events",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-events",
      "options": [
        {
          "name": "user-id",
          "key": "userId",
          "required": true,
          "description": "Azure AD user ID or UPN whose calendar to read. Requires `Calendars.Read.Shared` access (granted by the calendar owner)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-shared-calendar-events --user-id 'colleague@contoso.com'",
      "responseShape": "collection of Microsoft Graph `event` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-shared-calendar-view",
      "summary": "Return a date-windowed calendar view from another user's primary calendar (shared / delegated access). Recurrences expanded into individual occurrences.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/users/{user-id}/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-calendarview",
      "options": [
        {
          "name": "user-id",
          "key": "userId",
          "required": true,
          "description": "Azure AD user ID or UPN of the calendar owner."
        },
        {
          "name": "start-date-time",
          "key": "startDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "end-date-time",
          "key": "endDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-shared-calendar-view --user-id 'colleague@contoso.com' --start-date-time '2026-04-01T00:00:00Z' --end-date-time '2026-05-01T00:00:00Z'",
      "responseShape": "collection of Microsoft Graph `event` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-shared-insights",
      "summary": "List documents *shared with* the signed-in user, scored by Microsoft's relevance ranking — sibling to `list-shared-with-me` but with sharing-context details (`sharingHistory[]`, `lastShared.sharedBy`, `lastShared.sharingReference`).",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/insights/shared",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/insights-list-shared",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-shared-insights",
      "responseShape": "collection of Microsoft Graph `sharedInsight` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-shared-mailbox-folder-messages",
      "summary": "List messages in a single folder of a shared / delegated mailbox.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/users/{user-id}/mailFolders/{mail-folder-id}/messages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/mailfolder-list-messages",
      "options": [
        {
          "name": "user-id",
          "key": "userId",
          "required": true,
          "description": "Azure AD user ID or UPN of the shared mailbox."
        },
        {
          "name": "mail-folder-id",
          "key": "mailFolderId",
          "required": true,
          "description": "Mail folder ID or well-known name (`inbox`, `sentitems`, etc.) inside that mailbox."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-shared-mailbox-folder-messages --user-id 'shared-mailbox@contoso.com' --mail-folder-id 'inbox'",
      "responseShape": "collection of Microsoft Graph `message` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-shared-mailbox-messages",
      "summary": "List messages from a shared or delegated mailbox the signed-in user has read access to. Same shape as `list-mail-messages` but scoped to a specific mailbox owner. 403 if the signed-in user does not have shared access to that mailbox.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/users/{user-id}/messages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-messages",
      "options": [
        {
          "name": "user-id",
          "key": "userId",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-shared-mailbox-messages --user-id 'shared-mailbox@contoso.com'",
      "responseShape": "collection of Microsoft Graph `message` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-shared-with-me",
      "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.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/drive/sharedWithMe",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-sharedwithme",
      "options": [],
      "example": "ask-marcel list-shared-with-me",
      "responseShape": "collection of Microsoft Graph `driveItem` resources under `value[]` (each with a `remoteItem` pointer)"
    },
    {
      "name": "list-sharepoint-list-columns",
      "summary": "List the column definitions (schema) of a SharePoint list. Useful before reading list items so you know which fields exist and their types. Note: Graph silently ignores `$top` and `$skip` on this endpoint, so the CLI exposes only `--select` and `--expand`.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/lists/{list-id}/columns",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/list-list-columns",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "list-id",
          "key": "listId",
          "required": true,
          "description": "List ID inside the site. Returned by `list-sharepoint-site-lists`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-list-columns --site-id 'contoso.sharepoint.com,abc...,def...' --list-id 'list-guid' --select 'name,displayName,readOnly'",
      "responseShape": "collection of Microsoft Graph `columnDefinition` resources under `value[]`"
    },
    {
      "name": "list-sharepoint-list-item-versions",
      "summary": "List the version history of a SharePoint list item — every change (column edits, status flips, custom-field changes) tracked as a `listItemVersion`. Distinct from `list-drive-item-versions`, which tracks file content versions.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/lists/{list-id}/items/{list-item-id}/versions",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/listitem-list-versions",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "list-id",
          "key": "listId",
          "required": true,
          "description": "List ID inside the site. Returned by `list-sharepoint-site-lists`."
        },
        {
          "name": "list-item-id",
          "key": "listItemId",
          "required": true,
          "description": "List item ID inside the list. Returned by `list-sharepoint-site-list-items`.",
          "aliases": [
            {
              "name": "item-id",
              "key": "itemId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-list-item-versions --site-id 'contoso.sharepoint.com,...' --list-id 'list-guid' --list-item-id '12'",
      "responseShape": "collection of Microsoft Graph `listItemVersion` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sharepoint-site-drives",
      "summary": "List the document libraries (drives) attached to a SharePoint site.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/drives",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/drive-list",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-site-drives --site-id 'contoso.sharepoint.com,1234,5678'",
      "responseShape": "collection of Microsoft Graph `drive` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sharepoint-site-list-items",
      "summary": "List the rows (listItem resources) of a single SharePoint list.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/lists/{list-id}/items",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/listitem-list",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
        },
        {
          "name": "list-id",
          "key": "listId",
          "required": true,
          "description": "SharePoint list ID or display name. Returned by `ask-marcel list-sharepoint-site-lists`.",
          "argumentHint": {
            "kind": "idOrName"
          }
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-site-list-items --site-id 'contoso.sharepoint.com,1234,5678' --list-id 'Tasks'",
      "responseShape": "collection of Microsoft Graph `listItem` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sharepoint-site-lists",
      "summary": "List all SharePoint lists (custom + built-in document libraries) on a site. Note: the skip flag is intentionally omitted — Graph rejects $skip on this endpoint with invalidRequest. Paginate via the top-level `nextLink` → `next-page`. Heads-up: when `top` is small, the FIRST page may legitimately be empty (`value: []`) while still carrying a `nextLink` — Graph filters server-side after slicing. Always check `nextLink` before concluding \"no lists\".",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/lists",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/list-list",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID. Returned by `ask-marcel search-sharepoint-sites-by-name`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-site-lists --site-id 'contoso.sharepoint.com,1234,5678'",
      "responseShape": "collection of Microsoft Graph `list` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sharepoint-site-onenote-notebook-sections",
      "summary": "List sections inside one OneNote notebook attached to a SharePoint site.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/onenote/notebooks/{notebook-id}/sections",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/notebook-list-sections",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "notebook-id",
          "key": "notebookId",
          "required": true,
          "description": "OneNote notebook ID inside the site."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-site-onenote-notebook-sections --site-id 'contoso.sharepoint.com,...' --notebook-id 'nb1'",
      "responseShape": "collection of Microsoft Graph `onenoteSection` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sharepoint-site-onenote-notebooks",
      "summary": "List OneNote notebooks attached to a SharePoint site (separate from the personal `list-onenote-notebooks` which targets `/me`).",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/onenote/notebooks",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/onenote-list-notebooks",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-site-onenote-notebooks --site-id 'contoso.sharepoint.com,...'",
      "responseShape": "collection of Microsoft Graph `notebook` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sharepoint-site-onenote-section-pages",
      "summary": "List pages inside one section of a SharePoint-site OneNote notebook.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/onenote/sections/{onenote-section-id}/pages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/section-list-pages",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "onenote-section-id",
          "key": "onenoteSectionId",
          "required": true,
          "description": "OneNote section ID inside the site.",
          "aliases": [
            {
              "name": "section-id",
              "key": "sectionId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-site-onenote-section-pages --site-id 'contoso.sharepoint.com,...' --onenote-section-id 's1'",
      "responseShape": "collection of Microsoft Graph `onenotePage` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-sharepoint-site-pages",
      "summary": "List modern SharePoint pages on a site (news posts, dashboards, landing pages). Each `sitePage` has `title`, `description`, `webUrl`, `publishingState`, `lastPublishedDateTime`. Returned items are the read-only listing — fetch the page body via the SharePoint REST API or by opening the `webUrl`.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/pages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/sitepage-list",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-sharepoint-site-pages --site-id 'contoso.sharepoint.com,...'",
      "responseShape": "collection of Microsoft Graph `sitePage` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-site-columns",
      "summary": "List the *site-level* column definitions — columns reusable across multiple lists in the site. Distinct from `list-sharepoint-list-columns` which returns one specific list's schema. Note: Graph silently ignores `$top` and `$skip` on this endpoint (verified live — passing them returns the full collection regardless), so the CLI exposes only `--select` and `--expand`.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/columns",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/site-list-columns",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID. Returned by `search-sharepoint-sites-by-name`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-site-columns --site-id 'contoso.sharepoint.com,...' --select 'name,displayName'",
      "responseShape": "collection of Microsoft Graph `columnDefinition` resources under `value[]`"
    },
    {
      "name": "list-site-content-types",
      "summary": "List the content type definitions of a SharePoint site — typed schemas (Document, Page, Item, custom-defined) describing which columns + behaviors apply to items of each type. Useful for understanding a site's information architecture.",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites/{site-id}/contentTypes",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/contenttype-list",
      "options": [
        {
          "name": "site-id",
          "key": "siteId",
          "required": true,
          "description": "SharePoint site ID."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-site-content-types --site-id 'contoso.sharepoint.com,...'",
      "responseShape": "collection of Microsoft Graph `contentType` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-specific-calendar-events",
      "summary": "List the events in a specific calendar (does not expand recurrences). `--calendar-id primary` (or `default`) routes to the signed-in user’s default calendar (`/me/calendar/events`); any other value goes to `/me/calendars/{id}/events` and must be a real calendar ID.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendars/{calendar-id}/events",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/calendar-list-events",
      "options": [
        {
          "name": "calendar-id",
          "key": "calendarId",
          "required": true,
          "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."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-specific-calendar-events --calendar-id 'primary'",
      "responseShape": "collection of Microsoft Graph `event` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-specific-calendar-view",
      "summary": "List the events in a specific calendar with recurrence expanded into individual occurrences in a date range. Both ISO date-time params are required by Graph. `--calendar-id primary` (or `default`) routes to the signed-in user’s default calendar.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/calendars/{calendar-id}/calendarView?startDateTime={start-date-time}&endDateTime={end-date-time}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/calendar-list-calendarview",
      "options": [
        {
          "name": "calendar-id",
          "key": "calendarId",
          "required": true,
          "description": "Calendar ID, or `primary` / `default` for the signed-in user’s default calendar. Returned by `ask-marcel list-calendars`."
        },
        {
          "name": "start-date-time",
          "key": "startDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "end-date-time",
          "key": "endDateTime",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-specific-calendar-view --calendar-id 'primary' --start-date-time '2026-04-01T00:00:00Z' --end-date-time '2026-05-01T00:00:00Z'",
      "responseShape": "collection of Microsoft Graph `event` resources (single occurrences) under `value[]`"
    },
    {
      "name": "list-team-channels",
      "summary": "List the channels (standard, private, shared) inside a single Microsoft Team. Microsoft documents this endpoint as supporting only `$filter` and `$select` — Graph returns `BadRequest` on `$top`, `$skip`, `$orderby`, `$expand`, so the CLI exposes only the two flags that actually work.",
      "category": "teams",
      "graphMethod": "GET",
      "graphPathTemplate": "/teams/{team-id}/channels",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/channel-list",
      "options": [
        {
          "name": "team-id",
          "key": "teamId",
          "required": true,
          "description": "Microsoft Teams team ID. Returned by `ask-marcel list-joined-teams`."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        }
      ],
      "example": "ask-marcel list-team-channels --team-id 'abc-1234-...'",
      "responseShape": "collection of Microsoft Graph `channel` resources under `value[]`"
    },
    {
      "name": "list-team-installed-apps",
      "summary": "List the Teams apps installed in a team. The CLI hard-pins `$expand=teamsAppDefinition` so every entry includes `displayName`, `version`, and `distributionMethod` (the bare endpoint returns only opaque IDs). Useful for surfacing which integrations are wired into a given team. Graph rejects user-supplied OData query parameters on this endpoint (`Query option 'Top' is not allowed`) — so the standard OData flags are intentionally NOT exposed here. The response itself is still server-paginated via `@odata.nextLink` when the team has many installed apps; chain with `next-page` to walk subsequent pages.",
      "category": "teams",
      "graphMethod": "GET",
      "graphPathTemplate": "/teams/{team-id}/installedApps?$expand=teamsAppDefinition",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/team-list-installedapps",
      "options": [
        {
          "name": "team-id",
          "key": "teamId",
          "required": true,
          "description": "Microsoft Teams team ID."
        }
      ],
      "example": "ask-marcel list-team-installed-apps --team-id 'tm1'",
      "responseShape": "collection of Microsoft Graph `teamsAppInstallation` resources under `value[]`, each with an inline `teamsAppDefinition` (`displayName`, `version`, `distributionMethod`)",
      "pagination": true
    },
    {
      "name": "list-teams-chat-history",
      "summary": "Deep read of a Microsoft Teams chat's message history via the IC3 substrate (`teams.microsoft.com/api/chatsvc/<region>/v1/...`). Unlike `list-teams-chat-messages` (which caps at the 200 most recent messages with no working pagination cursor), this command follows the server-provided `_metadata.syncState` URL backward through history, fetching up to `--page-size` * `--max-pages` messages per invocation (default 200 * 20 = 4000). Uses the IC3-audience bearer captured at login (same Teams web client identity as the basic Teams token). The CLI ships a slim default projection — each message is reduced to `id, sequenceId, composetime, originalarrivaltime, messagetype, from, imdisplayname, content` and `content` is truncated to 4096 chars (with `truncated: true` and `originalContentChars` set on the affected entries). Pass `--full true` to opt out of projection and truncation; pass `--max-content-chars N` to override the truncation cap. **Best-effort, may break on Microsoft client updates** — the IC3 substrate is not in the public Microsoft Graph API. To page beyond `--max-pages`, take the response's `nextSyncState` and pass it back as `--sync-state` on the next call.",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "https://teams.microsoft.com/api/chatsvc/{region}/v1/users/ME/conversations/{chat-id}/messages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chatmessage-list",
      "options": [
        {
          "name": "chat-id",
          "key": "chatId",
          "required": true,
          "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`."
        },
        {
          "name": "sync-state",
          "key": "syncState",
          "required": false,
          "description": "Opaque pagination URL returned in the prior response's `nextSyncState` field. Pass it back to continue paging backward from where the previous invocation stopped. Omit on the first call."
        },
        {
          "name": "page-size",
          "key": "pageSize",
          "required": false,
          "description": "Messages per IC3 page (positive integer; default 200). Server may silently cap."
        },
        {
          "name": "max-pages",
          "key": "maxPages",
          "required": false,
          "description": "Safety cap on the backward walk (positive integer; default 20). Each page can return up to `--page-size` messages, so default ceiling is ~4000 messages or ~4 MB inline JSON per invocation. Raise carefully — response is buffered fully before returning."
        },
        {
          "name": "full",
          "key": "full",
          "required": false,
          "description": "Pass `--full true` to return the raw IC3 substrate shape (every field on every message, no truncation). Default (`--full false`, or omitted) projects each message down to `id, sequenceId, composetime, originalarrivaltime, messagetype, from, imdisplayname, content` — covering 'who said what, when' without the IC3 envelope noise."
        },
        {
          "name": "max-content-chars",
          "key": "maxContentChars",
          "required": false,
          "description": "When the slim projection is active (i.e. `--full` is not `true`), cap each message `content` at this many characters; messages cut at the cap also carry `truncated: true` and `originalContentChars` so a consumer can decide whether to re-fetch with `--full true`. Default 4096. Ignored when `--full true` is set."
        }
      ],
      "example": "ask-marcel list-teams-chat-history --chat-id '19:abc...@unq.gbl.spaces' --max-pages 5",
      "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.**",
      "stability": "experimental"
    },
    {
      "name": "list-teams-chat-messages",
      "summary": "List the most recent messages in a single Microsoft Teams chat via the chat substrate. Companion to `list-teams-chats-with-messages` when the inlined `lastMessage` isn't deep enough. Uses the chatsvcagg-audience bearer captured at login. **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. **No pagination**: the route caps at the 200 most recent messages per chat and the CLI cannot reach older history (Teams web itself uses WebSockets for scrollback, and the official `Chat.Read` Graph scope that would enable paginated reads is outside the appid's scope ceiling).",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "https://teams.microsoft.com/api/csa/{region}/api/v1/chats/{chat-id}/messages",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chatmessage-list",
      "options": [
        {
          "name": "chat-id",
          "key": "chatId",
          "required": true,
          "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`."
        }
      ],
      "example": "ask-marcel list-teams-chat-messages --chat-id '19:abc...@unq.gbl.spaces'",
      "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).",
      "stability": "experimental"
    },
    {
      "name": "list-teams-chats-with-messages",
      "summary": "List the signed-in user's Microsoft Teams chats with the last message body inlined per chat. Uses the chatsvcagg-audience bearer captured at login. Paginated via `continuationToken` (default page size 100; pass the response's `continuationToken` back as `--continuation-token` while `hasMoreData: true`). **Best-effort, may break on Microsoft client updates**: the chat substrate is not part of the public Microsoft Graph API; Microsoft can change route shapes without notice. Caller Graph scopes do NOT matter here; the substrate server gates access on the appid + identity, not on Graph scopes.",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chat-list",
      "options": [
        {
          "name": "page-size",
          "key": "pageSize",
          "required": false,
          "description": "Chats per page (positive integer; default 100, same value Teams web uses). Server may silently cap."
        },
        {
          "name": "continuation-token",
          "key": "continuationToken",
          "required": false,
          "description": "Opaque pagination cursor returned in the prior response's `continuationToken` field. Omit on the first call; loop until `hasMoreData` is false."
        }
      ],
      "example": "ask-marcel list-teams-chats-with-messages --page-size 100",
      "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.**",
      "stability": "experimental"
    },
    {
      "name": "list-todo-linked-resources",
      "summary": "List the linked resources (URLs, emails, files) attached to a Microsoft To Do task.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/todo/lists/{todo-task-list-id}/tasks/{todo-task-id}/linkedResources",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/todotask-list-linkedresources",
      "options": [
        {
          "name": "todo-task-list-id",
          "key": "todoTaskListId",
          "required": true,
          "description": "To Do task list ID. Returned by `ask-marcel list-todo-task-lists`.",
          "aliases": [
            {
              "name": "task-list-id",
              "key": "taskListId"
            },
            {
              "name": "todo-list-id",
              "key": "todoListId"
            }
          ]
        },
        {
          "name": "todo-task-id",
          "key": "todoTaskId",
          "required": true,
          "description": "To Do task ID. Returned by `ask-marcel list-todo-tasks`. Accepts `--task-id` as a shorter alias (within this command's flag set the To Do context is unambiguous).",
          "aliases": [
            {
              "name": "task-id",
              "key": "taskId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-todo-linked-resources --todo-task-list-id 'AAMkAGI...' --todo-task-id 'AAMkABC...'",
      "responseShape": "collection of Microsoft Graph `linkedResource` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-todo-task-lists",
      "summary": "List the signed-in user's Microsoft To Do task lists (e.g. `Tasks`, `Flagged Emails`, custom lists). Note: Graph rejects `$select` and `$orderby` on this endpoint with `RequestBroker--ParseUri`, so the CLI does not expose those flags — slice / sort client-side.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/todo/lists",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/todo-list-lists",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-todo-task-lists",
      "responseShape": "collection of Microsoft Graph `todoTaskList` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-todo-tasks",
      "summary": "List every task in a single Microsoft To Do task list, regardless of completion status. Use `list-incomplete-todo-tasks` if you only want the open ones. Known Graph quirk: certain `--select` combinations (notably any combo that includes `title`) trip `RequestBroker--ParseUri` on this endpoint; the CLI rewrites that opaque error to a hint pointing at the workaround.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/todo/lists/{todo-task-list-id}/tasks",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/todotasklist-list-tasks",
      "options": [
        {
          "name": "todo-task-list-id",
          "key": "todoTaskListId",
          "required": true,
          "description": "To Do task list ID. Returned by `ask-marcel list-todo-task-lists`.",
          "aliases": [
            {
              "name": "task-list-id",
              "key": "taskListId"
            },
            {
              "name": "todo-list-id",
              "key": "todoListId"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-todo-tasks --todo-task-list-id 'AAMkAGI...'",
      "responseShape": "collection of Microsoft Graph `todoTask` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-todo-tasks-delta",
      "summary": "Track incremental task changes (added / updated / completed / deleted) within a single Microsoft To Do list. The first call returns the current snapshot plus `@odata.deltaLink`; subsequent calls with that link return only what has changed since. Note: Graph rejects standard OData query parameters on this delta endpoint (the page-cap flag throws `Skip token is not provided`), so the OData passthrough is intentionally NOT exposed here. Use `next-page` with the returned `@odata.nextLink` to walk pages.",
      "category": "tasks",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/todo/lists/{todo-task-list-id}/tasks/delta()",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/todotask-delta",
      "options": [
        {
          "name": "todo-task-list-id",
          "key": "todoTaskListId",
          "required": true,
          "description": "Microsoft To Do task list ID. Returned by `list-todo-task-lists`.",
          "aliases": [
            {
              "name": "task-list-id",
              "key": "taskListId"
            },
            {
              "name": "todo-list-id",
              "key": "todoListId"
            }
          ]
        }
      ],
      "example": "ask-marcel list-todo-tasks-delta --todo-task-list-id 'AAMkAD...'",
      "responseShape": "collection of Microsoft Graph `todoTask` resources under `data.value[]`. Cursor tokens are hoisted to envelope level: top-level `nextLink` while paging, then top-level `deltaLink` on the final page.",
      "pagination": true
    },
    {
      "name": "list-trending-insights",
      "summary": "List documents trending around the signed-in user — files popular in their working network (colleagues' recent edits, shares, opens). Microsoft's relevance ranking, useful for surfacing unfamiliar but related work.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/insights/trending",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/insights-list-trending",
      "options": [
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-trending-insights",
      "responseShape": "collection of Microsoft Graph `trending` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "list-user-direct-reports",
      "summary": "List a specific user's direct reports.",
      "category": "user",
      "graphMethod": "GET",
      "graphPathTemplate": "/users/{user-id}/directReports",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-directreports",
      "options": [
        {
          "name": "user-id",
          "key": "userId",
          "required": true,
          "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)."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel list-user-direct-reports --user-id 'alice@contoso.com'",
      "responseShape": "collection of Microsoft Graph `directoryObject` resources (typically `user`) under `value[]`",
      "pagination": true
    },
    {
      "name": "microsoft-search-query",
      "summary": "Run a federated KQL search across the signed-in user's mail, files, list items, sites, calendar events, and people. Microsoft Graph v1.0 rejects multi-entity search bodies on most tenants (`Multiple entity search is not supported in v1.0`), so this command issues SIX parallel POSTs — one per entityType — and merges the per-entity `searchHits` containers into a single `value[]`. Each container is identifiable by the resource type inside `hits[].resource`. If a sub-request fails (e.g. tenant lacks the scope for one entity), the others still return; failures show up in `partialErrors[]`. Page size is fixed at 25 per sub-request and `top` is NOT exposed (Graph rejects $top in /search/query bodies). `chatMessage` is excluded since `Chat.Read*` is unavailable.",
      "category": "meta",
      "graphMethod": "POST",
      "graphPathTemplate": "/search/query",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/search-query",
      "options": [
        {
          "name": "query",
          "key": "query",
          "required": true,
          "description": "KQL query string. Supports field operators where indexed by the corpus (e.g. `from:alice`, `subject:\"q3 budget\"`, `filetype:xlsx`). Free-text works everywhere."
        }
      ],
      "example": "ask-marcel microsoft-search-query --query 'q3 budget'",
      "responseShape": "merged Microsoft Graph `searchResponse` envelope: `{ value: [{ searchTerms, hitsContainers: [{ total, hits: [{ hitId, rank, summary, resource }] }] }, …], partialErrors?: [{ entityType, error }] }`. value[] holds one container per entityType that succeeded; partialErrors[] (only present when at least one sub-request failed) lists which entityTypes returned errors.",
      "bodyTemplate": "{ requests: [{ entityTypes: ['<one-of-driveItem-listItem-site-message-event-person>'], query: { queryString: '{query}' }, size: 25 }] } — sent six times in parallel, one per entityType"
    },
    {
      "name": "my-quick-context",
      "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.",
      "category": "meta",
      "graphMethod": "GET",
      "graphPathTemplate": "(meta) parallel: /me, /me/drive, /me/mailFolders/inbox, /me/calendar, /me/planner/plans, /me/onenote/notebooks, /me/joinedTeams, /me/drive/recent, /me/mailboxSettings",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-get",
      "options": [],
      "example": "ask-marcel my-quick-context",
      "responseShape": "`{ user: { id, displayName, userPrincipalName, mail, jobTitle? }, primaryDriveId?, inboxId?, primaryCalendarId?, primaryPlannerPlanId?, defaultNotebookId?, firstJoinedTeamId?, recentDriveItemId?, tenantTimeZone?, tenantLocale?, tenantWorkingHours?: { start, end, timeZone? } }` — every field except `user.id` is optional and absent when its source call failed. `user.jobTitle` is the user's role string from Azure AD (e.g. \"Engineering Manager\"). `tenantTimeZone` is the Outlook timezone string (e.g. \"Romance Standard Time\", \"Pacific Standard Time\"); `tenantLocale` is the IETF tag (e.g. \"en-US\"). For Microsoft To Do lists, call `list-todo-task-lists` separately — they were dropped from this command's fan-out to keep the envelope LLM-tractable."
    },
    {
      "name": "next-page",
      "summary": "Fetch the next page of a paginated Graph response. Pass the cursor the previous command emitted — in text mode that is the `next: <url>` value in the `---` footer; in JSON mode it is the top-level `nextLink` field. Never reach into `data[\"@odata.nextLink\"]`; the CLI strips that and surfaces it as a first-class envelope/footer field. Automatically signs `/me/chats` and `/chats/...` cursors with the M365ChatClient elevated token to match the chat-metadata commands.",
      "category": "meta",
      "graphMethod": "GET",
      "graphPathTemplate": "{url}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/paging",
      "options": [
        {
          "name": "url",
          "key": "url",
          "required": true,
          "description": "Full Graph v1.0 URL — copy the top-level `nextLink` field from the previous response (the CLI hoists Graph's `@odata.nextLink` out of `data` to envelope level). Example: `https://graph.microsoft.com/v1.0/me/messages?$skiptoken=AKDsfg...`. Loop: keep calling until the response no longer contains `nextLink`. Also handles `deltaLink` (also hoisted) if you want to resume a delta query.",
          "argumentHint": {
            "kind": "graphSubpath"
          }
        }
      ],
      "example": "ask-marcel next-page --url 'https://graph.microsoft.com/v1.0/me/messages?$skip=10'",
      "responseShape": "same shape as the originating endpoint — `{ ok: true, data: { value: [...] }, nextLink: \"...\" }` with the cursor at envelope level."
    },
    {
      "name": "resolve-calendar-link",
      "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.",
      "category": "calendar",
      "graphMethod": "GET",
      "graphPathTemplate": "{url}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/event-get",
      "options": [
        {
          "name": "url",
          "key": "url",
          "required": true,
          "description": "Outlook web URL for a single calendar item. Accepted hosts: `outlook.office.com`, `outlook.office365.com`, `outlook.live.com`. Accepted shapes: path-style (`/calendar/item/AAMkA...`) and OWA query-style with calendar path (`/owa/?itemid=AAMkA...&path=/calendar/item`). Mail links (`/mail/...` or `/owa/?itemid=...` without `path=/calendar`) are rejected with `cli_reject_mail_link_on_calendar_resolver` — use `resolve-mail-link` for those."
        }
      ],
      "example": "ask-marcel resolve-calendar-link --url 'https://outlook.office.com/calendar/item/AAMkAGI2THVS...'",
      "responseShape": "`{ eventId: string }`. `eventId` is URL-decoded and ready to pass to `get-calendar-event --event-id <id>`."
    },
    {
      "name": "resolve-drive-share-link",
      "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`.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "{url}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/shares-get",
      "options": [
        {
          "name": "url",
          "key": "url",
          "required": true,
          "description": "A OneDrive / SharePoint sharing URL — the address from the \"Copy link\" / \"Share\" action in the OneDrive or SharePoint UI. Examples: `https://contoso.sharepoint.com/:b:/s/sitename/EaB1cD...`, `https://contoso-my.sharepoint.com/personal/user_contoso_com/Documents/file.pdf`, `https://1drv.ms/b/s!AbCdEfGh...`. The CLI does not follow the redirect on `1drv.ms` links — the short URL itself is encoded as the share token (Graph resolves it on the server side)."
        }
      ],
      "example": "ask-marcel resolve-drive-share-link --url 'https://contoso.sharepoint.com/:b:/s/team/EaB1cD2eF...?e=abc'",
      "responseShape": "`{ shareToken: string, graphPath: string, originalUrl: string }`. `shareToken` is the `u!<base64url>` form. `graphPath` is the ready-to-use `/shares/{token}/driveItem` URL — pass it to `ask-marcel next-page --url <link>` for a one-shot driveItem fetch, or feed the `shareToken` into any future `/shares/{token}/...` endpoint. `originalUrl` is echoed back for round-trip confirmation."
    },
    {
      "name": "resolve-mail-link",
      "summary": "Parse a Microsoft Outlook web mail link (the URL emitted by the \"Copy link\" / address-bar share of an email) into its `messageId`. Pure transformation — no Graph call. Pipe the result into `get-mail-message` to fetch the body, or `convert-mail-to-markdown` to render it. For Outlook calendar links use `resolve-calendar-link` instead — this command rejects them with a pointer.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "{url}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/message-get",
      "options": [
        {
          "name": "url",
          "key": "url",
          "required": true,
          "description": "Outlook web URL for a single mail message. Accepted hosts: `outlook.office.com`, `outlook.office365.com`, `outlook.live.com`. Accepted shapes: OWA query-style (`/owa/?itemid=AAMkA...` or `/owa/?ItemID=AAMkA...`), modern path-style (`/mail/inbox/id/AAMkA...`), legacy short (`/mail/AAMkA...`). Calendar links (`/calendar/item/...` or `?path=/calendar/item`) are rejected with `cli_reject_calendar_link_on_mail_resolver` — use `resolve-calendar-link` for those."
        }
      ],
      "example": "ask-marcel resolve-mail-link --url 'https://outlook.office.com/mail/inbox/id/AAMkAGI2THVS...'",
      "responseShape": "`{ messageId: string }`. `messageId` is URL-decoded and ready to pass to `get-mail-message --message-id <id>` or `convert-mail-to-markdown --message-id <id>`."
    },
    {
      "name": "resolve-teams-link",
      "summary": "Parse a Microsoft Teams `Copy link` URL (the share link emitted by the message context menu in Teams) into its `chatId` + `messageId` components. Pure transformation — no Graph call. Pipe the result into `get-teams-chat-message` to fetch the message body, or into `list-teams-chat-history` to read the chat that contains it.",
      "category": "chats",
      "graphMethod": "GET",
      "graphPathTemplate": "{url}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/chatmessage-get",
      "options": [
        {
          "name": "url",
          "key": "url",
          "required": true,
          "description": "Teams message link, copied from Teams web/desktop via the message's `Copy link` action. Expected shape: `https://teams.microsoft.com/l/message/<url-encoded-chat-id>/<message-id>?tenantId=...&groupId=...&ctx=...`"
        }
      ],
      "example": "ask-marcel resolve-teams-link --url 'https://teams.microsoft.com/l/message/19%3A...%40unq.gbl.spaces/1700000000000?tenantId=...&groupId=...&ctx=chat'",
      "responseShape": "`{ chatId: string, messageId: string, tenantId?: string, groupId?: string, parentMessageId?: string, context?: string }`. `chatId` and `messageId` are URL-decoded and ready to pass to other commands. Optional fields are included only when the source URL carried them."
    },
    {
      "name": "scopes-check",
      "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.",
      "category": "meta",
      "graphMethod": "GET",
      "graphPathTemplate": "(meta) cached-token introspection — no Graph endpoint",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/permissions-reference",
      "options": [],
      "example": "ask-marcel scopes-check",
      "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`)."
    },
    {
      "name": "search-mail-messages",
      "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.",
      "category": "mail",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/messages?$search=\"{query}\"",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/user-list-messages",
      "options": [
        {
          "name": "query",
          "key": "query",
          "required": true,
          "description": "KQL or free-text query. Searches subject, body, sender, and recipients. Examples: `Q3 budget`, `from:alice@contoso.com`, `subject:invoice received>=2026-01-01`."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel search-mail-messages --query 'from:alice subject:Q3'",
      "responseShape": "collection of Microsoft Graph `message` resources under `value[]`, ranked by relevance, each projected to the default `--select` set (or the requested fields when overridden). The default omits `body`, `internetMessageHeaders`, and `uniqueBody`.",
      "pagination": true
    },
    {
      "name": "search-my-documents",
      "summary": "Search the signed-in user’s default OneDrive for documents matching a free-text query (filename, content, metadata).",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/drive/search(q='{query}')",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-search",
      "options": [
        {
          "name": "query",
          "key": "query",
          "required": true,
          "description": "Free-text search query. Matches filename, content, and metadata across the user’s personal OneDrive. Use `search-onedrive-files --drive-id <id>` instead to target a specific shared SharePoint or OneDrive drive by ID."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel search-my-documents --query 'q1 budget'",
      "responseShape": "collection of Microsoft Graph `driveItem` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "search-onedrive-files",
      "summary": "Search a single OneDrive / SharePoint drive for files and folders matching a free-text query.",
      "category": "drive",
      "graphMethod": "GET",
      "graphPathTemplate": "/drives/{drive-id}/search(q='{query}')",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/driveitem-search",
      "options": [
        {
          "name": "drive-id",
          "key": "driveId",
          "required": true,
          "description": "Microsoft Graph drive ID to search inside. Returned by `ask-marcel list-drives`."
        },
        {
          "name": "query",
          "key": "query",
          "required": true,
          "description": "Free-text search query. Matches filename, content, and metadata."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel search-onedrive-files --drive-id 'b!1234' --query 'q1 budget'",
      "responseShape": "collection of Microsoft Graph `driveItem` resources under `value[]`",
      "pagination": true
    },
    {
      "name": "search-onenote-pages",
      "summary": "Find OneNote pages whose title contains a substring (case-sensitive — page content is NOT searched). Microsoft removed full-text OneNote `?search=` from v1.0 Graph; only $filter against `title` remains, which is what this command runs. Accepts the OData passthrough flags top/skip/select/orderby/expand. The filter passthrough is intentionally omitted — the path already pins a `$filter` for the title-contains predicate, and Graph rejects two `$filter` query params.",
      "category": "notes",
      "graphMethod": "GET",
      "graphPathTemplate": "/me/onenote/pages?$filter=contains(title,'{title-substring}')",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/onenote-list-pages",
      "options": [
        {
          "name": "title-substring",
          "key": "titleSubstring",
          "required": true,
          "description": "Substring to look for inside OneNote page titles (case-sensitive, exact substring). This is title-only — full-text body search is not available on OneNote pages in v1.0 Graph. Use `list-onenote-section-pages` if you already know the section.",
          "aliases": [
            {
              "name": "query",
              "key": "query"
            }
          ]
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel search-onenote-pages --title-substring 'meeting notes' --top 25",
      "responseShape": "collection of Microsoft Graph `onenotePage` resources under `value[]` whose title contains the substring",
      "pagination": true
    },
    {
      "name": "search-sharepoint-sites-by-name",
      "summary": "Search the tenant for SharePoint sites whose display name or description matches a free-text query (returns up to 25).",
      "category": "sharepoint",
      "graphMethod": "GET",
      "graphPathTemplate": "/sites?search={query}",
      "graphDocsUrl": "https://learn.microsoft.com/en-us/graph/api/site-search",
      "options": [
        {
          "name": "query",
          "key": "query",
          "required": true,
          "description": "Free-text query. Matches site display name and description across the tenant."
        },
        {
          "name": "top",
          "key": "top",
          "required": false,
          "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."
        },
        {
          "name": "skip",
          "key": "skip",
          "required": false,
          "description": "OData $skip: skip the first N items before returning results (non-negative integer). Useful with $top for offset paging."
        },
        {
          "name": "select",
          "key": "select",
          "required": false,
          "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`."
        },
        {
          "name": "filter",
          "key": "filter",
          "required": false,
          "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."
        },
        {
          "name": "orderby",
          "key": "orderby",
          "required": false,
          "description": "OData $orderby: sort expression with optional asc/desc (e.g. `receivedDateTime desc`). Some Graph filter combinations are rejected; remove $orderby if InefficientFilter occurs."
        },
        {
          "name": "expand",
          "key": "expand",
          "required": false,
          "description": "OData $expand: navigation properties to include inline (e.g. `attachments`). Increases response size; use sparingly."
        }
      ],
      "example": "ask-marcel search-sharepoint-sites-by-name --query 'marketing'",
      "responseShape": "collection of Microsoft Graph `site` resources under `value[]` (up to 25)",
      "pagination": true
    }
  ]
}
