ask-marcel-office-cli 1.5.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -3
- package/README.md +23 -59
- package/dist/cli.js +1757 -1766
- package/dist/commands.json +437 -275
- package/dist/composition/build-deps.d.ts +7 -8
- package/dist/composition/cli.d.ts +5 -4
- package/dist/domain/iso-datetime.d.ts +1 -1
- package/dist/index.js +1606 -1653
- package/dist/infra/auth.d.ts +37 -21
- package/dist/infra/browser-auth.d.ts +13 -12
- package/dist/infra/graph-client.d.ts +28 -2
- package/dist/presenter/error-hints.d.ts +9 -9
- package/dist/use-cases/commands/build-command.d.ts +1 -1
- package/dist/use-cases/commands/command-types.d.ts +5 -5
- package/dist/use-cases/commands/convert-local-file.d.ts +4 -0
- package/dist/use-cases/commands/create-forward-draft.d.ts +12 -0
- package/dist/use-cases/commands/create-reply-draft.d.ts +10 -0
- package/dist/use-cases/commands/docs.d.ts +1 -1
- package/dist/use-cases/commands/get-schedule.d.ts +11 -0
- package/dist/use-cases/commands/graph-scopes.d.ts +1 -1
- package/dist/use-cases/commands/image-extraction.d.ts +2 -1
- package/dist/use-cases/commands/iso-datetime-schema.d.ts +1 -1
- package/dist/use-cases/commands/login-status.d.ts +31 -0
- package/dist/use-cases/commands/login.d.ts +3 -1
- package/dist/use-cases/commands/markdown-pipeline.d.ts +5 -5
- package/dist/use-cases/commands/option-descriptions.d.ts +1 -1
- package/dist/use-cases/commands/output-path.d.ts +4 -4
- package/dist/use-cases/commands/parse-recipients.d.ts +14 -0
- package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +4 -2
- package/dist/use-cases/ports/filesystem.d.ts +1 -1
- package/docs/COMMANDS.md +16 -13
- package/docs/USAGE.md +23 -23
- package/package.json +3 -4
- package/dist/composition/env.d.ts +0 -2
- package/dist/infra/system-browser-auth.d.ts +0 -35
- package/dist/infra/system-browser-loader.d.ts +0 -3
- package/dist/infra/token-callback-server.d.ts +0 -31
- package/docs/commands.json +0 -7676
package/docs/COMMANDS.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Command reference
|
|
2
2
|
|
|
3
|
-
All
|
|
3
|
+
All 180 commands across 11 categories, grouped by category. Each row shows the command name, a one-line summary, the required parameters, and the underlying Microsoft Graph endpoint. The five lifecycle commands (`login`, `logout`, `update`, `docs`, `help-json`) are listed separately in the **Authentication & lifecycle** section below — `help-json` counts them in its manifest total (184), so a 179-vs-184 gap is those five, not a drift.
|
|
4
4
|
|
|
5
5
|
The auto-generated tables below are rebuilt from the live command registry on every `bun run docs:gen` / `bun run build` — they cannot drift from the actual surface.
|
|
6
6
|
|
|
7
7
|
For everything else:
|
|
8
8
|
|
|
9
|
-
- **Per-command runtime docs** — `ask-marcel docs <command>` (Markdown to stdout)
|
|
10
|
-
- **Per-command CLI help** — `ask-marcel <command> --help` (terse, ~60s to read)
|
|
9
|
+
- **Per-command runtime docs** — `ask-marcel-office docs <command>` (Markdown to stdout)
|
|
10
|
+
- **Per-command CLI help** — `ask-marcel-office <command> --help` (terse, ~60s to read)
|
|
11
11
|
- **Machine-readable manifest** — [`docs/commands.json`](commands.json) (full metadata: options, schema, examples, response shape, Graph docs URL)
|
|
12
12
|
- **Output format, pagination, library API, configuration** — [`docs/USAGE.md`](USAGE.md)
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ For everything else:
|
|
|
17
17
|
|---------|-------------|
|
|
18
18
|
| `login` | Authenticate (cached → refresh → browser fallback) |
|
|
19
19
|
| `logout` | Clear cached tokens |
|
|
20
|
-
| `update` | Update ask-marcel to the latest version on npm (auto-detects npm vs bun) |
|
|
20
|
+
| `update` | Update ask-marcel-office to the latest version on npm (auto-detects npm vs bun) |
|
|
21
21
|
| `docs <cmd>` | Print Markdown docs for a single command |
|
|
22
22
|
| `help-json [--terse] [--category <name>]` | Machine-readable manifest, full or filtered |
|
|
23
23
|
| `next-page --url <link>` | Walk a paginated response by feeding the previous `nextLink` |
|
|
@@ -29,10 +29,10 @@ For everything else:
|
|
|
29
29
|
| Command | Description | Required params | Graph endpoint |
|
|
30
30
|
|---------|-------------|-----------------|----------------|
|
|
31
31
|
| `convert-drive-item-zip` | Unzip a `.zip` from a OneDrive / SharePoint item and convert every contained file in one call — so "read the handover archive" doesn't need a separate unzip + per-file conversion. Office files (docx/xlsx/pptx/odt/ods/odp and their macro-enabled / template variants) are converted to markdown via the local pipelines; plain-text entries (txt/md/csv/json/yaml/…) are decoded inline; legacy OLE .xls (sheetjs) and .doc (word-extractor, text only) are extracted; an Outlook .msg entry is rendered to markdown (headers + body, with its own attachments converted recursively); PDFs have their text layer extracted (text/plain); images, binaries, nested archives, legacy .ppt, and scanned/image-only PDFs (no text layer) are listed with a note (not unpacked) so one unsupported entry never fails the whole archive. Pass `--include-metadata true` to append each Office file's side-channel metadata block. Capped at 100 entries (the archive is buffered in memory); beyond that the response is flagged `truncated`. | `--drive-id`, `--item-id`, `--include-metadata` | `GET /drives/{drive-id}/items/{item-id}/content` |
|
|
32
|
-
| `download-drive-item-as-markdown` | Download a OneDrive / SharePoint file converted to markdown via local conversion pipelines. Supported: docx (mammoth → turndown; embedded images are replaced with `[image]` placeholders by default — pass `--inline-images true` to embed them as base64 `data:` URIs, or pull the full-resolution originals with `extract-drive-item-images`; tables become GFM pipe tables), xlsx (one markdown table per sheet via sheetjs; any sheet whose used range exceeds the `--max-cells` cap, default 50 000, is summarised with a band-by-band read hint instead of a multi-hundred-MB table), csv (rendered as a markdown table), odt/ods/odp (OpenDocument body walked from content.xml — headings, lists, tables, named sheets, per-slide text, including style-hidden content), plus plain-text passthrough (txt/md/html/json/yaml/log/xml/etc.) — the bytes are followed through any CDN redirect and returned inline as `{ contentType: "text/plain", size, text }` so the LLM never needs a separate fetch step. Loop/Fluid/Whiteboard files use Graph `?format=html` (the four inputs Microsoft documents — https://learn.microsoft.com/en-us/graph/api/driveitem-get-content-format). pptx is flattened to per-slide text (titles, bullets, text-box / table text, with speaker notes inline) as `## Slide N` sections — document order, not guaranteed visual order; for layout / images / charts, `download-drive-item-as-pdf` + a vision-capable LLM reads the rendered deck better. pdf has its text layer extracted locally (via unpdf) and returned as `{ contentType: "text/plain", size, text }`; a scanned / image-only PDF with no text layer points you at `download-drive-item-as-pdf` + a vision model. Legacy OLE Office: .xls (Excel 97-2003) is read by sheetjs (same markdown-table path as .xlsx); .doc (Word 97-2003) is extracted by word-extractor as plain text (body only, no structure); .ppt (PowerPoint 97-2003) has no pure-JS path — use `download-drive-item-as-pdf` first, then a vision model. Outlook .msg (a saved email, also an OLE container) is rendered to markdown — an H1 subject, a From/To/Cc/Date header block, the message body, then an `## Attachments` section where each attachment is converted recursively through this same pipeline (the same way a .zip is unpacked); inline / image attachments are listed with a note. For rtf/etc. use `download-drive-item-as-pdf` — Graph `?format=pdf` accepts 38 input extensions. | `--drive-id`, `--item-id`, `--include-metadata`, `--inline-images`, `--max-cells` | `GET /drives/{drive-id}/items/{item-id}/content?format=html` |
|
|
32
|
+
| `download-drive-item-as-markdown` | Download a OneDrive / SharePoint file converted to markdown via local conversion pipelines. Supported: docx (mammoth → turndown; embedded images are replaced with `[image]` placeholders by default — pass `--inline-images true` to embed them as base64 `data:` URIs, or pull the full-resolution originals with `extract-drive-item-images`; tables become GFM pipe tables), xlsx (one markdown table per sheet via sheetjs; any sheet whose used range exceeds the `--max-cells` cap, default 50 000, is summarised with a band-by-band read hint instead of a multi-hundred-MB table), csv (rendered as a markdown table), odt/ods/odp (OpenDocument body walked from content.xml — headings, lists, tables, named sheets, per-slide text, including style-hidden content), plus plain-text passthrough (txt/md/html/json/yaml/log/xml/etc.) — the bytes are followed through any CDN redirect and returned inline as `{ contentType: "text/plain", size, text }` so the LLM never needs a separate fetch step. Loop/Fluid/Whiteboard files use Graph `?format=html` (the four inputs Microsoft documents — https://learn.microsoft.com/en-us/graph/api/driveitem-get-content-format). To locate `.loop` pages to convert, run `microsoft-search-query` with `filetype:loop` and pass the `resource.id` and `parentReference.driveId` from each hit here. pptx is flattened to per-slide text (titles, bullets, text-box / table text, with speaker notes inline) as `## Slide N` sections — document order, not guaranteed visual order; for layout / images / charts, `download-drive-item-as-pdf` + a vision-capable LLM reads the rendered deck better. pdf has its text layer extracted locally (via unpdf) and returned as `{ contentType: "text/plain", size, text }`; a scanned / image-only PDF with no text layer points you at `download-drive-item-as-pdf` + a vision model. Legacy OLE Office: .xls (Excel 97-2003) is read by sheetjs (same markdown-table path as .xlsx); .doc (Word 97-2003) is extracted by word-extractor as plain text (body only, no structure); .ppt (PowerPoint 97-2003) has no pure-JS path — use `download-drive-item-as-pdf` first, then a vision model. Outlook .msg (a saved email, also an OLE container) is rendered to markdown — an H1 subject, a From/To/Cc/Date header block, the message body, then an `## Attachments` section where each attachment is converted recursively through this same pipeline (the same way a .zip is unpacked); inline / image attachments are listed with a note. For rtf/etc. use `download-drive-item-as-pdf` — Graph `?format=pdf` accepts 38 input extensions. | `--drive-id`, `--item-id`, `--include-metadata`, `--inline-images`, `--max-cells` | `GET /drives/{drive-id}/items/{item-id}/content?format=html` |
|
|
33
33
|
| `download-drive-item-as-pdf` | Download a OneDrive / SharePoint file converted to PDF on the fly by Graph (`?format=pdf`). Source must be one of the Office formats Graph supports — doc, docx, ppt, pptx, xls, xlsx, rtf, csv, odp, ods, odt, etc. The command pre-fetches the filename and short-circuits to a raw download in two cases: plain-text source extensions (txt, md, html, json, …) where conversion is meaningless, and `pdf` sources where the source IS already a PDF (Graph’s `?format=pdf` does not list `pdf` in its supported input set — the CDN responds 406 InputFormatNotSupported on `pdf → pdf`). Worst-case wall-clock is two back-to-back Graph round-trips; the `?format=pdf` transform can run up to the 5-minute request timeout on large or complex sources. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content?format=pdf` |
|
|
34
34
|
| `download-drive-item-content` | Download the binary content of a file stored in OneDrive / SharePoint, with the bytes inlined. The CLI follows the Graph 302 → SharePoint media-transform redirect internally so the LLM never has to fetch an external URL. The bytes are CONTENT-SNIFFED, not judged by extension: if they decode as valid UTF-8 they come back as `{contentType: "text/plain", size, text}` (avoids ~33% base64 bloat, works for any text file regardless of name); otherwise as `{contentType, size, base64}`. A binary file that happens to be named `.txt` is returned faithfully as base64 — never silently corrupted into `�` by a forced text decode. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
|
|
35
|
-
| `download-drive-item-version` | Download a *non-current* historical version of a OneDrive / SharePoint file. `--format original` (default) returns the raw bytes — Graph refuses to serve the current version through this endpoint with "You cannot get the content of the current version"; for the current version use `download-drive-item-content`. `--format pdf` runs Graph `?format=pdf` for Office docs; plain-text and `pdf` sources short-circuit to raw bytes with `passthrough: true` + a note (Graph rejects `pdf → pdf` with InputFormatNotSupported). `--format markdown` runs the local conversion pipeline (mammoth for docx, sheetjs for xlsx, csv → table, odt/ods/odp via content.xml, plain-text passthrough). All three formats use an M365ChatClient-elevated Graph token (captured at login from m365.cloud.microsoft) — the Teams web client token returns 403 logicalPermissionAccessDenied on historical-version stream content. The CLI follows the SharePoint streamContent redirect internally so the LLM never has to fetch an external URL.
|
|
35
|
+
| `download-drive-item-version` | Download a *non-current* historical version of a OneDrive / SharePoint file. `--format original` (default) returns the raw bytes — Graph refuses to serve the current version through this endpoint with "You cannot get the content of the current version"; for the current version use `download-drive-item-content`. `--format pdf` runs Graph `?format=pdf` for Office docs; plain-text and `pdf` sources short-circuit to raw bytes with `passthrough: true` + a note (Graph rejects `pdf → pdf` with InputFormatNotSupported). `--format markdown` runs the local conversion pipeline (mammoth for docx, sheetjs for xlsx, csv → table, odt/ods/odp via content.xml, plain-text passthrough). All three formats use an M365ChatClient-elevated Graph token (captured at login from m365.cloud.microsoft) — the Teams web client token returns 403 logicalPermissionAccessDenied on historical-version stream content. The CLI follows the SharePoint streamContent redirect internally so the LLM never has to fetch an external URL. caveat for `--format pdf`: Graph sometimes silently falls back to raw source bytes for the current version (which Graph occasionally serves through this endpoint) — when the response carries `passthrough: true`, save with the source extension, not `.pdf` (the global output-path flag refuses the mismatch). | `--drive-id`, `--item-id`, `--version-id`, `--format`, `--include-metadata` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content` |
|
|
36
36
|
| `extract-drive-item-images` | Extract the embedded images from a OneDrive / SharePoint document. For docx / xlsx / pptx (and their macro-enabled / template variants) it reads the OOXML media parts directly (png/jpg/gif/bmp/tiff/webp/svg) — including original full-resolution / un-cropped originals and images on hidden slides the rendered view never shows. For a pdf it walks every page via unpdf and re-encodes each painted image as PNG (note: page-oriented — it captures images as painted on each page, but NOT layer-hidden/unpainted XObjects or the full uncropped original behind a clipped image). Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded in the response. svg rides back as its XML source (which carries the diagram text labels); legacy vector (emf/wmf) and audio/video are skipped. For any other format the command returns a 415 pointing at `download-drive-item-content`. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
|
|
37
37
|
| `extract-sharepoint-links-in-documents` | Find every `*.sharepoint.com` URL embedded in a Word / Excel / PowerPoint or OpenDocument file on OneDrive or SharePoint and resolve each one to its driveItem (driveId, itemId, name, webUrl) so the agent can feed those into `download-drive-item-as-pdf` / `-as-markdown` etc. The document sibling of `extract-sharepoint-links-in-mail`. For OOXML (.docx/.xlsx/.pptx) it reads external hyperlinks from the package’s relationship parts (`_rels/*.rels`, `TargetMode="External"`); for OpenDocument (.odt/.ods/.odp) it reads the inline `xlink:href` links in content.xml / styles.xml — either way it catches links wherever they live (body text, headers/footers, cell formulas, slide shapes). Read-only — no conversion happens here. Capped at 25 unique URLs per call (returns `truncated: true` and `skippedCount` when there are more); duplicates are deduplicated; per-link errors are captured inside each entry instead of failing the whole call. Non-zip inputs (pdf/images) return an api_error. | `--drive-id`, `--item-id` | `GET /drives/{drive-id}/items/{item-id}/content` |
|
|
38
38
|
| `get-drive-delta` | Get the incremental change set (added / modified / deleted items) under a OneDrive / SharePoint folder. Use the `@odata.deltaLink` from a previous response to resume. | `--drive-id`, `--item-id`, `--top`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /drives/{drive-id}/items/{item-id}/delta()` |
|
|
@@ -125,8 +125,10 @@ For everything else:
|
|
|
125
125
|
| `convert-mail-attachment-to-markdown` | Convert an Outlook mail attachment to markdown. Polymorphic on the attachment’s `@odata.type`: fileAttachment decodes the inline bytes and runs them through the local conversion pipeline (docx via mammoth, xlsx via sheetjs, csv as markdown table, odt/ods/odp via content.xml, pptx as per-slide text (titles + bullets + speaker notes inline), pdf via text-layer extraction (unpdf → text/plain), legacy .xls via sheetjs and legacy .doc via word-extractor (text only), an Outlook .msg attachment rendered to markdown — headers + body with its own attachments converted recursively — plus plain-text passthrough); referenceAttachment resolves via /shares/{token}/driveItem and routes through the same dispatcher; itemAttachment (embedded mail / event / contact) is rendered locally via dedicated renderers. For pptx layout / images, `convert-mail-attachment-to-pdf` + a vision model reads the rendered deck better. A scanned / image-only PDF (no text layer), legacy .ppt, and rtf/etc. point to the PDF sibling. Loop/Fluid/Whiteboard reference-attachments use Graph `?format=html` (the four inputs Microsoft documents). | `--message-id`, `--attachment-id`, `--include-metadata` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
126
126
|
| `convert-mail-attachment-to-pdf` | Convert an Outlook mail attachment to PDF on the fly. Polymorphic on the attachment’s `@odata.type`: fileAttachment uploads the bytes to a temp folder under /me/drive (large files use Graph’s chunked upload session — no 4 MB ceiling), runs ?format=pdf, then deletes the temp item; referenceAttachment resolves via /shares/{token}/driveItem and runs ?format=pdf in place; plain-text source extensions and `pdf` sources short-circuit to a raw-bytes envelope on either path (Graph’s `?format=pdf` does not accept `pdf` as an input format — pdf attachments are returned as-is). itemAttachment (embedded mail/event/contact) is unsupported here — Graph rejects those source types — use convert-mail-attachment-to-markdown instead. Worst-case wall-clock for huge attachments is ~22 minutes (1 metadata GET + up-to-20 chunk PUTs + 1 convert GET + 1 cleanup DELETE, each capped at 60s). | `--message-id`, `--attachment-id` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
127
127
|
| `convert-mail-attachment-zip` | Unzip a `.zip` Outlook mail attachment and convert every contained file in one call — the mail-side mirror of `convert-drive-item-zip`, so reading a zipped vendor deck doesn't need `get-mail-attachment` + manual `unzip` + per-file conversion. Pulls the fileAttachment bytes, unzips them (legacy GBK / CP437 entry names — Chinese vendor archives written by WinRAR / Windows Explorer — are decoded correctly, not mojibaked), and runs each file through the local pipelines: Office files (docx/xlsx/pptx/odt/ods/odp and macro-enabled / template variants) → markdown; plain-text entries decoded inline; legacy OLE .xls (sheetjs) and .doc (word-extractor, text only) extracted; an inner Outlook .msg rendered; PDFs have their text layer extracted; images, binaries, nested archives, legacy .ppt, and scanned/image-only PDFs are listed with a note (not unpacked) so one unsupported entry never fails the whole archive. Pass `--include-metadata true` to append each Office file's side-channel metadata block. Capped at 100 entries; beyond that the response is flagged `truncated`. itemAttachment / referenceAttachment are rejected (no inline zip payload). | `--message-id`, `--attachment-id`, `--include-metadata` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
128
|
-
| `convert-mail-to-markdown` | Render a single Outlook email as markdown — headers (`**Subject:**`, `**From:**`, `**To:**`, `**Cc:**` only when present, `**Date:**`), followed by the body run through turndown. By default, inline images (`isInline:true` + `image/*` content-type, size ≤ 2 MB) are embedded as base64 `data:` URIs so the output is self-contained (non-image inline attachments are NOT embedded; oversize inline images are replaced with a placeholder note). For LLM callers that only want the text body, pass `--inline-images false` to skip the per-image bytes fetch entirely — the body keeps raw `cid:<contentId>` references and the inline images surface in the file-attachments list so you can decide whether to fetch them separately via `get-mail-attachment`. File attachments are always listed below the body by name + size + id; their bytes are NOT fetched here — call `convert-mail-attachment-to-pdf` or `get-mail-attachment` with the id when you actually need them. Staged-fetch design
|
|
128
|
+
| `convert-mail-to-markdown` | Render a single Outlook email as markdown — headers (`**Subject:**`, `**From:**`, `**To:**`, `**Cc:**` only when present, `**Date:**`), followed by the body run through turndown. By default, inline images (`isInline:true` + `image/*` content-type, size ≤ 2 MB) are embedded as base64 `data:` URIs so the output is self-contained (non-image inline attachments are NOT embedded; oversize inline images are replaced with a placeholder note). For LLM callers that only want the text body, pass `--inline-images false` to skip the per-image bytes fetch entirely — the body keeps raw `cid:<contentId>` references and the inline images surface in the file-attachments list so you can decide whether to fetch them separately via `get-mail-attachment`. File attachments are always listed below the body by name + size + id; their bytes are NOT fetched here — call `convert-mail-attachment-to-pdf` or `get-mail-attachment` with the id when you actually need them. Staged-fetch design: one call for the body, one for the attachments-metadata list (only if `hasAttachments:true`), and one per small inline image — replaces the old `?$expand=attachments` which timed out / truncated on messages with multi-MB attachments. | `--message-id`, `--inline-images`, `--keep-quoted` | `GET /me/messages/{message-id}` |
|
|
129
|
+
| `create-forward-draft` | Create an UNSENT forward draft of an existing message. POST /me/messages/{id}/createForward mints the draft (FW: subject, quoted original) with your comment placed above the quote and the recipients set, in one call. Redirects a thread to the right owner without leaving the CLI. The draft is saved in Drafts and can be reviewed, edited, and sent from any Outlook client; the CLI still cannot send. | `--forward-message-id`, `--to-recipients`, `--cc-recipients`, `--body-content`, `--subject` | `POST /me/messages/{forward-message-id}/createForward (+ optional body-free PATCH for cc / subject)` |
|
|
129
130
|
| `create-mail-draft` | Create a new mail draft. POST /me/messages (or /me/mailFolders/{id}/messages when --mail-folder-id is set). The draft is saved in the Drafts folder (or the specified folder) and can be sent later via the Outlook client or Graph sendMail. Recipients are comma-separated email addresses. Returns the created message object with its id — use this id with update-mail-draft to modify the draft before sending. | `--subject`, `--body-content`, `--body-content-type`, `--to-recipients`, `--cc-recipients`, `--bcc-recipients`, `--importance`, `--mail-folder-id` | `POST /me/messages (or /me/mailFolders/{mail-folder-id}/messages)` |
|
|
131
|
+
| `create-reply-draft` | Create an UNSENT reply-all draft threaded on an existing message. POST /me/messages/{id}/createReplyAll mints the draft (inherited recipients, RE: subject, quoted history) with your reply text placed above the quote, in one call. Reply-all by design - dropping recipients is a deliberate act for the human in Outlook, not a default. The draft is saved in Drafts and can be reviewed, edited, and sent from any Outlook client; the CLI still cannot send. | `--reply-to-message-id`, `--body-content`, `--subject` | `POST /me/messages/{reply-to-message-id}/createReplyAll (+ optional body-free PATCH for subject)` |
|
|
130
132
|
| `extract-mail-attachment-images` | Extract the embedded images from an Outlook mail attachment that is a pdf or a docx / xlsx / pptx (and their macro-enabled / template variants). OOXML reads the media parts directly (png/jpg/gif/bmp/tiff/webp/svg), including full-resolution / un-cropped originals and images on hidden slides; pdf walks every page via unpdf and re-encodes each painted image as PNG (page-oriented — not layer-hidden/unpainted/uncropped originals). fileAttachment decodes the inline bytes; referenceAttachment resolves via /shares/{token}/driveItem and fetches the content. Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded. svg rides back as its XML source (which carries the diagram text labels); legacy vector (emf/wmf) and audio/video are skipped. itemAttachment and unsupported formats return a 415. | `--message-id`, `--attachment-id` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
131
133
|
| `extract-sharepoint-links-in-mail` | Find every `*.sharepoint.com` URL in the body of a single Outlook email and resolve each one to its driveItem (driveId, itemId, name, webUrl) so the agent can feed those into `download-drive-item-as-pdf` / `-as-markdown` etc. Read-only — no conversion happens here. Capped at 25 unique URLs per call to bound fan-out (returns `truncated: true` and `skippedCount` when the body has more); duplicate URLs are deduplicated. Per-link errors are captured inside each entry instead of failing the whole call. | `--message-id` | `GET /me/messages/{message-id}` |
|
|
132
134
|
| `get-mail-attachment` | Get a single attachment on an Outlook message (metadata, plus the base64 `contentBytes` for file attachments). For fileAttachments, the response also carries a `base64` mirror of `contentBytes` so the global output-path flag can land the bytes on disk in one call — and when an output-path is set the CLI strips BOTH `contentBytes` and `base64` from stdout, leaving a compact metadata envelope with `savedTo` (the file is on disk; no multi-MB base64 in the terminal). When you only want metadata, use `--select id,name,contentType,size` to skip the `contentBytes` payload. | `--message-id`, `--attachment-id`, `--select`, `--expand` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
@@ -179,7 +181,7 @@ For everything else:
|
|
|
179
181
|
| `get-group` | Return metadata for a single Azure AD / Microsoft 365 group. Use `--select` to slim large group payloads (the full group resource includes 30+ fields). | `--group-id`, `--select`, `--expand` | `GET /groups/{group-id}` |
|
|
180
182
|
| `get-my-manager` | Return the signed-in user's manager (a single `user` resource). When no manager is set in the directory, Graph returns 404 `Request_ResourceNotFound`; this command maps that one specific 404 to `{ ok: true, data: { manager: null, note: '...' } }` so an LLM can distinguish 'no manager' from a permission failure without parsing prose. Use `--select` to slim the response (e.g. `--select id,displayName,mail`). | `--select`, `--expand` | `GET /me/manager` |
|
|
181
183
|
| `get-my-profile-photo` | Download the signed-in user's profile photo (largest available size), inlined. The CLI follows the Graph 302 → CDN redirect internally so the LLM never has to fetch an external URL. | _(none)_ | `GET /me/photo/$value` |
|
|
182
|
-
| `get-organization` | Return the tenant's organization metadata — display name, country, verified domains, business phones, technical / security notification contacts, assigned Microsoft 365 SKUs / licensing. Graph wraps the single organization resource under `value[]` (
|
|
184
|
+
| `get-organization` | Return the tenant's organization metadata — display name, country, verified domains, business phones, technical / security notification contacts, assigned Microsoft 365 SKUs / licensing. Graph wraps the single organization resource under `value[]` (— even though only one tenant exists, the endpoint returns a collection). The full resource is ~57 KB; use `--select` to slim it (e.g. `--select id,displayName,verifiedDomains`). | `--select`, `--expand` | `GET /organization` |
|
|
183
185
|
| `get-user-manager` | Return a specific user's manager (a single `user` resource). When the user has no manager set in the directory, Graph returns 404 `Request_ResourceNotFound`; this command maps that one specific 404 to `{ ok: true, data: { manager: null, note: '...' } }` (same shape as `get-my-manager`) so an LLM can distinguish 'no manager' from 'unknown user' with a single discriminator across both commands. Use `--select` to slim the response. | `--user-id`, `--select`, `--expand` | `GET /users/{user-id}/manager` |
|
|
184
186
|
| `list-group-members` | List members of an Azure AD / Microsoft 365 group. Returns users, groups, and other directoryObjects depending on the group's membership. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/members` |
|
|
185
187
|
| `list-group-owners` | List the owners of an Azure AD / Microsoft 365 group. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/owners` |
|
|
@@ -199,6 +201,7 @@ For everything else:
|
|
|
199
201
|
| `convert-calendar-event-attachment-to-pdf` | Convert an attachment on an Outlook calendar event to PDF on the fly (shares the mail-attachment pipeline). fileAttachment uploads the bytes to a temp folder under /me/drive, runs Graph `?format=pdf`, then deletes the temp item; referenceAttachment resolves via /shares/{token}/driveItem and converts in place; plain-text and `pdf` sources short-circuit to a raw-bytes envelope (Graph’s `?format=pdf` does not accept `pdf` as an input). image attachments are rejected (Graph rejects image inputs); itemAttachment (embedded mail/event/contact) is unsupported — use convert-calendar-event-attachment-to-markdown. Best for the deck attached to a meeting, where PDF preserves slide layout for a vision-capable LLM. | `--event-id`, `--attachment-id` | `GET /me/events/{event-id}/attachments/{attachment-id}` |
|
|
200
202
|
| `get-calendar-event` | Fetch a single calendar event by ID from the signed-in user’s default calendar. Pass `--select` to project only the fields you need (the full event body can be large with HTML body and attendee lists). | `--event-id`, `--select`, `--expand` | `GET /me/events/{event-id}` |
|
|
201
203
|
| `get-my-calendar` | Return metadata for the signed-in user's *primary* calendar — `id`, `name`, `color`, `owner`, `canShare`, `canViewPrivateItems`, `canEdit`, `defaultOnlineMeetingProvider`. Sibling to `list-calendars` which returns every calendar (incl. shared / subscribed). Use `--select` to fetch only the fields you need. | `--select`, `--expand` | `GET /me/calendar` |
|
|
204
|
+
| `get-schedule` | Get the free/busy availability of one or more people (or meeting rooms) over a time window — the Outlook "scheduling assistant" data. Pass a comma-separated list of email addresses; each result carries `availabilityView` (one character per interval: 0 free, 1 tentative, 2 busy, 3 out-of-office, 4 working-elsewhere), the underlying `scheduleItems[]` (busy blocks with start/end and, where the target's calendar permits, subject/location), and the person's `workingHours`. Read-only despite being a POST (the body is a query, nothing is created). Bounds are interpreted as UTC; per-address failures (unknown mailbox, external tenant) surface inside that entry's `error` field rather than failing the whole call. | `--schedules`, `--start-date-time`, `--end-date-time`, `--availability-view-interval` | `POST /me/calendar/getSchedule` |
|
|
202
205
|
| `get-specific-calendar-event` | Fetch a single calendar event by ID from a specific calendar. `--calendar-id primary` (or `default`) targets the signed-in user's default calendar. Use `--select` to slim large event payloads (a typical event with body+attendees runs >50 KB). | `--calendar-id`, `--event-id`, `--select`, `--expand` | `GET /me/calendars/{calendar-id}/events/{event-id}` |
|
|
203
206
|
| `list-calendar-event-attachments` | List the attachments (file, item, reference) on a single Outlook calendar event. Ships an opinionated default `--select=id,name,contentType,size,isInline` so an LLM doesn't accidentally pull multi-MB `contentBytes` for every attachment. The `@odata.type` discriminator is always returned by Graph regardless of `$select` (and Graph rejects asking for it explicitly). To read one, call `convert-calendar-event-attachment-to-markdown` (or `convert-calendar-event-attachment-to-pdf` for slide / layout fidelity). | `--event-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/events/{event-id}/attachments` |
|
|
204
207
|
| `list-calendar-event-instances` | List the individual occurrences of a recurring calendar event over a date range. Both ISO date-time params are required by Graph. `--calendar-id` is optional and defaults to `primary` (the signed-in user’s default calendar) — most callers know the event-id but not which calendar it lives in. Pass an explicit `--calendar-id` only when targeting a non-default calendar. | `--calendar-id`, `--event-id`, `--start-date-time`, `--end-date-time`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/calendars/{calendar-id}/events/{event-id}/instances?startDateTime={start-date-time}&endDateTime={end-date-time}` |
|
|
@@ -223,7 +226,7 @@ For everything else:
|
|
|
223
226
|
|
|
224
227
|
| Command | Description | Required params | Graph endpoint |
|
|
225
228
|
|---------|-------------|-----------------|----------------|
|
|
226
|
-
| `find-chats-with-user` | Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name
|
|
229
|
+
| `find-chats-with-user` | Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Alex` matches `Alex Kim` AND `alex.kim@example.com` AND `ALEX` — 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). Cross-tenant resolution: the summary roster returns externally-homed counterparts as a bare object-id (no name/email), which a name search cannot match; for every bare DIRECT (1:1) chat the command hydrates the roster via the per-chat members endpoint and re-matches — so an external counterpart who is bare in your 1:1 is still found, even when they were already resolved in some meeting (the dual-identity case). Bare members in group/meeting chats are not deep-probed; when nothing matches and such members exist it returns a `hint` plus `unresolvedMemberCount` rather than a confident empty result. **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. | `--name`, `--max-pages`, `--page-size` | `GET https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats` |
|
|
227
230
|
| `get-chat` | Return metadata for a single Microsoft Teams chat (1:1, group, or meeting). The CLI ships a slim default `--select=id,topic,chatType,createdDateTime,lastUpdatedDateTime`; pass `--select id,topic,webUrl,onlineMeetingInfo` (or any other comma-separated field list) to widen. Pass `--expand members` to inline membership. Returns metadata only — not the messages (which need `Chat.Read*`). Requires the M365ChatClient elevated token captured at login (the basic Teams web client token lacks `Chat.ReadBasic`). | `--chat-id`, `--select`, `--expand` | `GET /chats/{chat-id}` |
|
|
228
231
|
| `get-teams-chat-message` | Return a single Microsoft Teams chat message by its id via the chat substrate. Uses the chatsvcagg-audience bearer captured at login (same identity as the basic Teams token, different audience). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. Source the chat-id + message-id via `list-teams-chats-with-messages` or `list-teams-chat-messages`. | `--chat-id`, `--message-id` | `GET https://teams.microsoft.com/api/csa/{region}/api/v1/chats/{chat-id}/messages/{message-id}` |
|
|
229
232
|
| `list-chat-members` | List the members of a single Microsoft Teams chat. Graph rejects `$top` / `$orderby` / `$expand` on this endpoint, so the CLI advertises only the subset Graph honours (`--skip`, `--select`, `--filter`). | `--chat-id`, `--skip`, `--select`, `--filter` | `GET /chats/{chat-id}/members` |
|
|
@@ -249,11 +252,11 @@ For everything else:
|
|
|
249
252
|
|
|
250
253
|
| Command | Description | Required params | Graph endpoint |
|
|
251
254
|
|---------|-------------|-----------------|----------------|
|
|
252
|
-
| `convert-local-file` | Convert a file ON DISK to markdown — the only command that never calls Microsoft Graph (works offline, no login). Runs the same local pipelines as `download-drive-item-as-markdown`: docx (mammoth → turndown), xlsx (sheetjs tables, `--max-cells` OOM cap), pptx (per-slide text), odt/ods/odp, csv, pdf (text layer via unpdf), legacy OLE .xls / .doc, Outlook .msg (headers + body, attachments converted recursively), plain-text passthrough — and a `.zip` is unpacked with every contained file converted in one call (legacy GBK / CP437 entry names decoded, not mojibaked). What it canNOT do locally: convert TO pdf, and Loop/Fluid/Whiteboard sources — both need a Graph server round-trip (upload to OneDrive and use the drive-item siblings). Pass `--include-metadata true` for the Office side-channel metadata blocks; `--inline-images true` to embed docx images as base64 data URIs. | `--path`, `--include-metadata`, `--inline-images`, `--max-cells` | `GET (local) reads {path} from the local filesystem; not a Graph endpoint` |
|
|
255
|
+
| `convert-local-file` | Convert a file ON DISK to markdown — the only command that never calls Microsoft Graph (works offline, no login). Runs the same local pipelines as `download-drive-item-as-markdown`: docx (mammoth → turndown), xlsx (sheetjs tables, `--max-cells` OOM cap), pptx (per-slide text), odt/ods/odp, csv, pdf (text layer via unpdf), legacy OLE .xls / .doc, Outlook .msg (headers + body, attachments converted recursively), plain-text passthrough — and a `.zip` is unpacked with every contained file converted in one call (legacy GBK / CP437 entry names decoded, not mojibaked). What it canNOT do locally: convert TO pdf, and Loop/Fluid/Whiteboard sources — both need a Graph server round-trip (upload to OneDrive and use the drive-item siblings). Pass `--include-metadata true` for the Office side-channel metadata blocks; `--inline-images true` to embed docx images as base64 data URIs. | `--path`, `--include-metadata`, `--inline-images`, `--include-images`, `--max-cells` | `GET (local) reads {path} from the local filesystem; not a Graph endpoint` |
|
|
253
256
|
| `extract-local-file-images` | Extract the embedded images from a file ON DISK — the local sibling of `extract-drive-item-images`, and like `convert-local-file` it never calls Microsoft Graph (works offline, no login). Same per-extension dispatch: docx / xlsx / pptx (and their macro-enabled / template variants) have their OOXML media parts read directly (png/jpg/gif/bmp/tiff/webp/svg — full-resolution originals, including images on hidden slides); a pdf is walked page by page via unpdf with each painted image re-encoded as PNG. Two flows only this command completes: a Graph-rendered PDF saved locally (legacy `.ppt` → `download-drive-item-as-pdf` with the global output-path flag → this command pulls the slide images for OCR), and Office files unpacked from a local archive. Pair with the global output-dir flag to write every image to a folder; otherwise the bytes ride back base64-encoded. Any other extension returns a 415 naming the local ways out. | `--path` | `GET (local) reads {path} from the local filesystem; not a Graph endpoint` |
|
|
254
|
-
| `microsoft-search-query` | Run a federated KQL search across the signed-in user's mail, files, list items, sites, calendar events, and people. Microsoft Graph v1.0 rejects multi-entity search bodies on most tenants (`Multiple entity search is not supported in v1.0`), so this command issues SIX parallel POSTs — one per entityType — and merges the per-entity `searchHits` containers into a single `value[]`. Each container is identifiable by the resource type inside `hits[].resource`. If a sub-request fails (e.g. tenant lacks the scope for one entity), the others still return; failures show up in `partialErrors[]`. Page size is fixed at 25 per sub-request and `top` is NOT exposed (Graph rejects $top in /search/query bodies). `chatMessage` is excluded since `Chat.Read*` is unavailable. | `--query` | `POST /search/query` |
|
|
255
|
-
| `my-quick-context` | One-shot discovery for the IDs every other command needs, plus the user's job title and tenant timezone / locale / working-hours. Issues 9 Graph calls in parallel and returns what each succeeded for. Partial-result mode: only `/me` is load-bearing — if any other sub-call fails (missing license, scope, or tenant policy) the corresponding field is `undefined` but the rest are still returned. Replaces the audit's 5-call discovery chain — feed the IDs straight into `list-mail-folder-messages`, `list-folder-files`, `list-planner-tasks`, `list-onenote-notebook-sections`, etc. For Microsoft To Do lists call `list-todo-task-lists` on demand (intentionally dropped from this command's fan-out — the array of {id, displayName, wellknownListName} entries crowded the envelope with IDs an LLM rarely needs on first contact).
|
|
257
|
+
| `microsoft-search-query` | Run a federated KQL search across the signed-in user's mail, files, list items, sites, calendar events, and people. Microsoft Graph v1.0 rejects multi-entity search bodies on most tenants (`Multiple entity search is not supported in v1.0`), so this command issues SIX parallel POSTs — one per entityType — and merges the per-entity `searchHits` containers into a single `value[]`. Each container is identifiable by the resource type inside `hits[].resource`. If a sub-request fails (e.g. tenant lacks the scope for one entity), the others still return; failures show up in `partialErrors[]`. Page size is fixed at 25 per sub-request and `top` is NOT exposed (Graph rejects $top in /search/query bodies). `chatMessage` is excluded since `Chat.Read*` is unavailable. To find Microsoft Loop pages (`.loop`) for markdown conversion, query `filetype:loop`: each `driveItem` hit carries `resource.id` plus `resource.parentReference.driveId`, the exact pair `download-drive-item-as-markdown` needs to render the page via Graph `?format=html`. (`filetype:fluid` returns nothing on this corpus; Loop pages index as `.loop`.) | `--query` | `POST /search/query` |
|
|
258
|
+
| `my-quick-context` | One-shot discovery for the IDs every other command needs, plus the user's job title and tenant timezone / locale / working-hours. Issues 9 Graph calls in parallel and returns what each succeeded for. Partial-result mode: only `/me` is load-bearing — if any other sub-call fails (missing license, scope, or tenant policy) the corresponding field is `undefined` but the rest are still returned. Replaces the audit's 5-call discovery chain — feed the IDs straight into `list-mail-folder-messages`, `list-folder-files`, `list-planner-tasks`, `list-onenote-notebook-sections`, etc. For Microsoft To Do lists call `list-todo-task-lists` on demand (intentionally dropped from this command's fan-out — the array of {id, displayName, wellknownListName} entries crowded the envelope with IDs an LLM rarely needs on first contact). `tenantTimeZone` lets an LLM stop treating every datetime as UTC on first contact. | _(none)_ | `GET (meta) parallel: /me, /me/drive, /me/mailFolders/inbox, /me/calendar, /me/planner/plans, /me/onenote/notebooks, /me/joinedTeams, /me/drive/recent, /me/mailboxSettings` |
|
|
256
259
|
| `next-page` | Fetch the next page of a paginated Graph response. Pass the cursor the previous command emitted — in text mode that is the `next: <url>` value in the `---` footer; in JSON mode it is the top-level `nextLink` field. Never reach into `data["@odata.nextLink"]`; the CLI strips that and surfaces it as a first-class envelope/footer field. Automatically signs `/me/chats` and `/chats/...` cursors with the M365ChatClient elevated token to match the chat-metadata commands. | `--url` | `GET {url}` |
|
|
257
|
-
| `scopes-check` | Decode the cached Teams web client access token and return its scopes, audience, and expiry without making a Graph call. Use this as a self-test before running a command an LLM expects to fail with `accessDenied` — if the required scope isn't in the returned list, the call will reject regardless of tenant config. Each command's `scopesRequired` field in `help-json` lists the scopes that command needs; intersect with the array returned here for a pre-flight check (pipe both through `jq` and diff). The `expiresInSeconds` field
|
|
260
|
+
| `scopes-check` | Decode the cached Teams web client access token and return its scopes, audience, and expiry without making a Graph call. Use this as a self-test before running a command an LLM expects to fail with `accessDenied` — if the required scope isn't in the returned list, the call will reject regardless of tenant config. Each command's `scopesRequired` field in `help-json` lists the scopes that command needs; intersect with the array returned here for a pre-flight check (pipe both through `jq` and diff). The `expiresInSeconds` field 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. The `elevated` block reports whether the *separate* M365ChatClient-elevated token (needed by the historical-version download / convert commands) is cached and still usable — so a fresh process can pre-flight `deep-scan`-style workloads instead of discovering a 403 mid-run; `available:false` when it is absent, expired, or within the same 5-minute buffer the download path applies. The `chatsvcagg` and `ic3` blocks report the two Teams-chat substrate tokens (used by `list-teams-chat*` / `find-chats-with-user`) the same way; both self-heal from the shared refresh token, so they are informational rather than a preflight gate. | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
|
|
258
261
|
|
|
259
262
|
<!-- AUTO-GENERATED-COMMANDS:END -->
|
package/docs/USAGE.md
CHANGED
|
@@ -15,15 +15,15 @@ npm i -g ask-marcel-office-cli
|
|
|
15
15
|
bun add -g ask-marcel-office-cli
|
|
16
16
|
|
|
17
17
|
# authenticate (cached → refresh → browser fallback)
|
|
18
|
-
ask-marcel login
|
|
18
|
+
ask-marcel-office login
|
|
19
19
|
|
|
20
20
|
# the rest is discoverable
|
|
21
|
-
ask-marcel --help # ~34 KB, one-sentence summaries
|
|
22
|
-
ask-marcel help-json --terse --category mail # ~6 KB JSON for one category
|
|
23
|
-
ask-marcel docs list-mail-messages # full per-command Markdown
|
|
21
|
+
ask-marcel-office --help # ~34 KB, one-sentence summaries
|
|
22
|
+
ask-marcel-office help-json --terse --category mail # ~6 KB JSON for one category
|
|
23
|
+
ask-marcel-office docs list-mail-messages # full per-command Markdown
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
`ask-marcel update` auto-detects whether the CLI was installed via npm or bun (based on the bin path) and reinstalls globally with the matching tool. From a clone you can keep using `bun run src/main.ts <command>` directly.
|
|
26
|
+
`ask-marcel-office update` auto-detects whether the CLI was installed via npm or bun (based on the bin path) and reinstalls globally with the matching tool. From a clone you can keep using `bun run src/main.ts <command>` directly.
|
|
27
27
|
|
|
28
28
|
The first launch prints a one-time notice if a newer version is on npm.
|
|
29
29
|
|
|
@@ -36,12 +36,12 @@ Every command writes its output as a single document to **stdout** (success or e
|
|
|
36
36
|
YAML-ish `key: value` lines, generally smaller than the JSON envelope on long listings (the win grows with page size and shrinks toward parity on small projected pages — a 3-message page is ~3.3 KB in either format). Errors render as `error: <message>` followed by `hint:`, `source:`, and (when Graph throttles) `retryAfter: Ns` lines so an LLM can match the line shape without parsing JSON. Designed for LLMs reading and summarising; not for piping into other tools.
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
$ ask-marcel get-current-user
|
|
39
|
+
$ ask-marcel-office get-current-user
|
|
40
40
|
id: 0c1d2e3f-…
|
|
41
|
-
displayName:
|
|
42
|
-
mail:
|
|
41
|
+
displayName: Jordan Avery
|
|
42
|
+
mail: jordan.avery@example.com
|
|
43
43
|
|
|
44
|
-
$ ask-marcel list-mail-folder-messages --mail-folder-id inbox --top 2
|
|
44
|
+
$ ask-marcel-office list-mail-folder-messages --mail-folder-id inbox --top 2
|
|
45
45
|
id: AAMkAGI2…
|
|
46
46
|
subject: Re: Q2 planning
|
|
47
47
|
from: alice@example.com
|
|
@@ -52,7 +52,7 @@ from: bob@example.com
|
|
|
52
52
|
|
|
53
53
|
--- next: https://graph.microsoft.com/v1.0/me/messages?$skip=2
|
|
54
54
|
|
|
55
|
-
$ ask-marcel get-mail-message --message-id "bad-id"
|
|
55
|
+
$ ask-marcel-office get-mail-message --message-id "bad-id"
|
|
56
56
|
error: ErrorInvalidIdMalformed: Id is malformed.
|
|
57
57
|
hint: The ID you passed isn't valid for this endpoint. Source IDs from a sibling `list-*` command (e.g. `list-mail-messages`, `list-folder-files`, `list-chats`) — never construct them by hand.
|
|
58
58
|
source: graph
|
|
@@ -102,9 +102,9 @@ The stable `{ok, data, nextLink?, deltaLink?, count?}` envelope, unambiguous for
|
|
|
102
102
|
Most `list-*`, `search-*`, and `*-delta` commands accept the standard OData query parameters as optional flags. Use them to shrink large responses on the fly — particularly important for context-window-bound LLM consumers:
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
ask-marcel list-mail-messages --top 5 --select id,subject,from,receivedDateTime
|
|
106
|
-
ask-marcel list-recent-files --filter "name eq 'budget.xlsx'" --orderby lastModifiedDateTime desc
|
|
107
|
-
ask-marcel list-folder-files --drive-id b!abc --item-id 01DEF --select id,name --top 10
|
|
105
|
+
ask-marcel-office list-mail-messages --top 5 --select id,subject,from,receivedDateTime
|
|
106
|
+
ask-marcel-office list-recent-files --filter "name eq 'budget.xlsx'" --orderby lastModifiedDateTime desc
|
|
107
|
+
ask-marcel-office list-folder-files --drive-id b!abc --item-id 01DEF --select id,name --top 10
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
The canonical set is `--top <n>`, `--skip <n>`, `--select <csv>`, `--filter <kql>`, `--orderby <kql>`, `--expand <nav>`. `--top` is capped at 1000 with a clear validation error (Graph silently truncates beyond that on every endpoint). **The CLI advertises only the flags the underlying Graph endpoint honors — flags Graph silently rejects or ignores are dropped from the option set, so the manifest never lies.**
|
|
@@ -124,10 +124,10 @@ Narrower variants (a sample — see each command's `--help` for the exact list):
|
|
|
124
124
|
Every `--start-date-time` / `--end-date-time` flag on the calendar-view family (`list-calendar-view`, `list-calendar-view-delta`, `list-specific-calendar-view`, `list-shared-calendar-view`, `list-group-calendar-view`, `list-calendar-event-instances`) accepts strict ISO 8601 (`2026-04-01T00:00:00Z`) AND a relative vocabulary, so an LLM doesn't have to compute timestamps before answering "what's on my calendar this week":
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
ask-marcel list-calendar-view --start-date-time "start-of-week" --end-date-time "end-of-week"
|
|
128
|
-
ask-marcel list-calendar-view --start-date-time "today" --end-date-time "+7d"
|
|
129
|
-
ask-marcel list-calendar-view --start-date-time "monday" --end-date-time "next-monday"
|
|
130
|
-
ask-marcel list-calendar-view --start-date-time "start-of-month" --end-date-time "end-of-month"
|
|
127
|
+
ask-marcel-office list-calendar-view --start-date-time "start-of-week" --end-date-time "end-of-week"
|
|
128
|
+
ask-marcel-office list-calendar-view --start-date-time "today" --end-date-time "+7d"
|
|
129
|
+
ask-marcel-office list-calendar-view --start-date-time "monday" --end-date-time "next-monday"
|
|
130
|
+
ask-marcel-office list-calendar-view --start-date-time "start-of-month" --end-date-time "end-of-month"
|
|
131
131
|
```
|
|
132
132
|
|
|
133
133
|
Accepted shapes (UTC, week starts Monday): strict ISO; date-only (`2026-04-01` → midnight UTC); past offsets `7d` / `1w` / `2h` / `30m`; future offsets `+7d` / `+1w`; named `now` / `today` / `yesterday` / `tomorrow`; weekday names (`monday`-`sunday` — most-recent occurrence including today); `last-<weekday>` / `next-<weekday>`; boundary anchors `start-of-week|month|year`, `end-of-week|month|year`. An unrecognised input returns a structured validation error listing every accepted shape — no second round-trip needed.
|
|
@@ -137,7 +137,7 @@ Accepted shapes (UTC, week starts Monday): strict ISO; date-only (`2026-04-01`
|
|
|
137
137
|
Every download / convert command (PDF, image, raw bytes, MIME, OneNote HTML, the markdown converters) returns its bytes as `{ contentType, size, base64 }` (binary) or `{ contentType, size, text }` (text). In default text mode the binary variant prints `binary: <contentType>, <size> bytes — use --output-path to save` rather than spilling base64 to stdout. For multi-MB payloads — a 5 MB PDF round-tripped through stdout would blow most LLM context windows — pass the **global** `--output-path <path>` flag and the CLI lands the bytes locally:
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
ask-marcel convert-mail-attachment-to-pdf \
|
|
140
|
+
ask-marcel-office convert-mail-attachment-to-pdf \
|
|
141
141
|
--message-id "AAMkAD..." --attachment-id "AAMkAD...attach1" \
|
|
142
142
|
--output-path /tmp/deck.pdf
|
|
143
143
|
# Text mode:
|
|
@@ -158,21 +158,21 @@ When a response contains a `nextLink` cursor, feed that URL back through `next-p
|
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
160
|
# page 1
|
|
161
|
-
ask-marcel --output json list-mail-folders > p1.json
|
|
161
|
+
ask-marcel-office --output json list-mail-folders > p1.json
|
|
162
162
|
|
|
163
163
|
# page 2..N — loop until nextLink is gone
|
|
164
164
|
next=$(jq -r '.nextLink // empty' p1.json)
|
|
165
165
|
while [ -n "$next" ]; do
|
|
166
|
-
ask-marcel --output json next-page --url "$next" > pN.json
|
|
166
|
+
ask-marcel-office --output json next-page --url "$next" > pN.json
|
|
167
167
|
next=$(jq -r '.nextLink // empty' pN.json)
|
|
168
168
|
done
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
Every paginated command advertises this in three places: `ask-marcel <cmd> --help` prints a `Pagination:` line, `ask-marcel docs <cmd>` adds a `**Pagination:**` field, and [`docs/commands.json`](commands.json) ships `"pagination": true` on each entry so agents can detect it programmatically.
|
|
171
|
+
Every paginated command advertises this in three places: `ask-marcel-office <cmd> --help` prints a `Pagination:` line, `ask-marcel-office docs <cmd>` adds a `**Pagination:**` field, and [`docs/commands.json`](commands.json) ships `"pagination": true` on each entry so agents can detect it programmatically.
|
|
172
172
|
|
|
173
173
|
## Quick context
|
|
174
174
|
|
|
175
|
-
`ask-marcel my-quick-context` returns `{ user, primaryDriveId, inboxId, todoLists, primaryCalendarId }` in a single round trip — five Graph calls in parallel. Use it as the first call in any LLM session that needs per-user IDs to feed into other commands.
|
|
175
|
+
`ask-marcel-office my-quick-context` returns `{ user, primaryDriveId, inboxId, todoLists, primaryCalendarId }` in a single round trip — five Graph calls in parallel. Use it as the first call in any LLM session that needs per-user IDs to feed into other commands.
|
|
176
176
|
|
|
177
177
|
## Library API
|
|
178
178
|
|
|
@@ -231,7 +231,7 @@ src/
|
|
|
231
231
|
|
|
232
232
|
`download-drive-item-version --format <original|pdf|markdown>` needs a Graph token whose `appid` is on Microsoft's ODSP allow-list — the Teams web client token returns 403 with `logicalPermissionAccessDenied` against historical-version bytes.
|
|
233
233
|
|
|
234
|
-
Login captures a *second* Graph token from `https://m365.cloud.microsoft/search` whose first-party identity is M365ChatClient (`c0ab8ce9-e9a0-42e7-b064-33d422df41f1`) — an app on the ODSP allow-list. It is stored alongside the Teams token (`elevated_access_token` / `elevated_expires_on` fields in the cache) and used only by the historical-version command. Refresh path is re-capture via a brief Edge launch — the persistent profile cookies do silent SSO when fresh; if the federated IdP session has lapsed
|
|
234
|
+
Login captures a *second* Graph token from `https://m365.cloud.microsoft/search` whose first-party identity is M365ChatClient (`c0ab8ce9-e9a0-42e7-b064-33d422df41f1`) — an app on the ODSP allow-list. It is stored alongside the Teams token (`elevated_access_token` / `elevated_expires_on` fields in the cache) and used only by the historical-version command. Refresh path is re-capture via a brief Edge launch — the persistent profile cookies do silent SSO when fresh; if the federated IdP session has lapsed, interactive sign-in completes inside the popup. If the elevated capture fails at login, every other command (including `list-chats` / `get-chat`, which use the regular Teams token) still works. Because the elevated token carries no refresh token of its own, a cache-hit `login` does not renew it; run `ask-marcel-office login --force` to re-capture every token (basic + elevated + the chatsvcagg / ic3 substrate tokens) in one browser pass. Both `login` and `scopes-check` report all four tokens' `{ available, expiresInSeconds? }` status so you can see which one is about to lapse before a command hits a 403.
|
|
235
235
|
|
|
236
236
|
## Configuration
|
|
237
237
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ask-marcel-office-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Microsoft Graph CLI + library \u2014 typed Bun/TypeScript wrapper around 150+ Graph operations (read + on-the-fly PDF/markdown conversion + federated Microsoft Search) reachable from a Teams browser-OAuth token.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vincent Delacourt <vincent.delacourt@adama-development.com>",
|
|
@@ -25,11 +25,10 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
|
-
"ask-marcel": "./dist/cli.js"
|
|
28
|
+
"ask-marcel-office": "./dist/cli.js"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist/",
|
|
32
|
-
"docs/commands.json",
|
|
33
32
|
"docs/COMMANDS.md",
|
|
34
33
|
"docs/USAGE.md",
|
|
35
34
|
"CHANGELOG.md",
|
|
@@ -69,7 +68,7 @@
|
|
|
69
68
|
"docs:gen": "bun run scripts/gen-docs.ts",
|
|
70
69
|
"build": "rm -rf dist && bun run docs:gen && bun run build:js && bun run build:types && bun run scripts/fix-dts-extensions.ts && bun run scripts/add-shebang.ts && cp docs/commands.json dist/commands.json",
|
|
71
70
|
"prepublishOnly": "bun run build && bun test && bun run lint:strict && bun run typecheck && bun run coverage",
|
|
72
|
-
"build:bin": "mkdir -p dist/bin && bun build src/main.ts --compile --target=bun-darwin-arm64 --external playwright --outfile dist/bin/ask-marcel-mac-arm64 && bun build src/main.ts --compile --target=bun-darwin-x64 --external playwright --outfile dist/bin/ask-marcel-mac-x64 && bun build src/main.ts --compile --target=bun-linux-x64 --external playwright --outfile dist/bin/ask-marcel-linux && bun build src/main.ts --compile --target=bun-windows-x64 --external playwright --outfile dist/bin/ask-marcel-windows.exe"
|
|
71
|
+
"build:bin": "mkdir -p dist/bin && bun build src/main.ts --compile --target=bun-darwin-arm64 --external playwright --outfile dist/bin/ask-marcel-office-mac-arm64 && bun build src/main.ts --compile --target=bun-darwin-x64 --external playwright --outfile dist/bin/ask-marcel-office-mac-x64 && bun build src/main.ts --compile --target=bun-linux-x64 --external playwright --outfile dist/bin/ask-marcel-office-linux && bun build src/main.ts --compile --target=bun-windows-x64 --external playwright --outfile dist/bin/ask-marcel-office-windows.exe"
|
|
73
72
|
},
|
|
74
73
|
"devDependencies": {
|
|
75
74
|
"@eslint/js": "^9.28.0",
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { AccessToken } from '../domain/access-token.js';
|
|
2
|
-
import type { Result } from '../domain/result.js';
|
|
3
|
-
import type { Logger } from '../use-cases/ports/logger.js';
|
|
4
|
-
type SystemBrowserAuthDeps = {
|
|
5
|
-
readonly logger: Logger;
|
|
6
|
-
readonly timeoutMs?: number;
|
|
7
|
-
readonly extensionTimeoutMs?: number;
|
|
8
|
-
};
|
|
9
|
-
type SystemBrowserTokenResult = {
|
|
10
|
-
readonly accessToken: AccessToken;
|
|
11
|
-
readonly refreshToken: string | null;
|
|
12
|
-
readonly elevatedAccessToken?: AccessToken | null;
|
|
13
|
-
readonly chatsvcaggAccessToken?: AccessToken | null;
|
|
14
|
-
readonly ic3AccessToken?: AccessToken | null;
|
|
15
|
-
readonly chatsvcaggRegion?: string;
|
|
16
|
-
};
|
|
17
|
-
type SystemBrowserAuthError = {
|
|
18
|
-
type: 'server_bind_failed';
|
|
19
|
-
message: string;
|
|
20
|
-
} | {
|
|
21
|
-
type: 'browser_open_failed';
|
|
22
|
-
message: string;
|
|
23
|
-
} | {
|
|
24
|
-
type: 'extension_timeout';
|
|
25
|
-
message: string;
|
|
26
|
-
} | {
|
|
27
|
-
type: 'invalid_token';
|
|
28
|
-
message: string;
|
|
29
|
-
} | {
|
|
30
|
-
type: 'cancelled';
|
|
31
|
-
message: string;
|
|
32
|
-
};
|
|
33
|
-
declare const authenticateViaSystemBrowser: (deps: SystemBrowserAuthDeps) => Promise<Result<SystemBrowserTokenResult, SystemBrowserAuthError>>;
|
|
34
|
-
export { authenticateViaSystemBrowser };
|
|
35
|
-
export type { SystemBrowserAuthDeps, SystemBrowserAuthError, SystemBrowserTokenResult };
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { Result } from '../domain/result.js';
|
|
2
|
-
import type { Logger } from '../use-cases/ports/logger.js';
|
|
3
|
-
type TokenCallbackPayload = {
|
|
4
|
-
readonly access_token: string;
|
|
5
|
-
readonly refresh_token?: string;
|
|
6
|
-
readonly elevated_access_token?: string;
|
|
7
|
-
readonly chatsvcagg_access_token?: string;
|
|
8
|
-
readonly ic3_access_token?: string;
|
|
9
|
-
readonly chatsvcagg_region?: string;
|
|
10
|
-
};
|
|
11
|
-
type TokenCallbackServer = {
|
|
12
|
-
readonly port: number;
|
|
13
|
-
readonly start: () => Promise<Result<TokenCallbackPayload, TokenCallbackError>>;
|
|
14
|
-
readonly stop: () => Promise<void>;
|
|
15
|
-
};
|
|
16
|
-
type TokenCallbackError = {
|
|
17
|
-
type: 'bind_failed';
|
|
18
|
-
message: string;
|
|
19
|
-
} | {
|
|
20
|
-
type: 'timeout';
|
|
21
|
-
message: string;
|
|
22
|
-
} | {
|
|
23
|
-
type: 'invalid_payload';
|
|
24
|
-
message: string;
|
|
25
|
-
} | {
|
|
26
|
-
type: 'server_closed';
|
|
27
|
-
message: string;
|
|
28
|
-
};
|
|
29
|
-
declare const createTokenCallbackServer: (logger: Logger, timeoutMs?: number) => TokenCallbackServer;
|
|
30
|
-
export { createTokenCallbackServer };
|
|
31
|
-
export type { TokenCallbackError, TokenCallbackPayload, TokenCallbackServer };
|