ask-marcel-office-cli 2.0.0 → 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 +66 -0
- package/README.md +7 -6
- package/dist/cli.js +785 -604
- package/dist/commands.json +75 -22
- package/dist/index.js +723 -576
- package/dist/infra/auth.d.ts +31 -1
- package/dist/infra/browser-auth.d.ts +3 -1
- package/dist/infra/graph-client.d.ts +26 -0
- 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 +0 -4
- package/dist/use-cases/commands/image-extraction.d.ts +2 -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/parse-recipients.d.ts +14 -0
- package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +3 -1
- package/docs/COMMANDS.md +5 -4
- package/docs/USAGE.md +1 -1
- package/package.json +1 -1
package/dist/infra/auth.d.ts
CHANGED
|
@@ -25,7 +25,9 @@ type ElevatedOutcome = {
|
|
|
25
25
|
reason: ElevatedFailureReason | 'unknown_error';
|
|
26
26
|
};
|
|
27
27
|
type AuthManager = {
|
|
28
|
-
getAccessToken: (
|
|
28
|
+
getAccessToken: (options?: {
|
|
29
|
+
force?: boolean;
|
|
30
|
+
}) => Promise<Result<AccessToken, AuthError>>;
|
|
29
31
|
/**
|
|
30
32
|
* Returns a Graph token issued for an app on Microsoft's ODSP
|
|
31
33
|
* `logicalPermissions` allow-list. Falls through cache → re-capture
|
|
@@ -69,6 +71,34 @@ type AuthManager = {
|
|
|
69
71
|
* as `getLastElevatedOutcome`.
|
|
70
72
|
*/
|
|
71
73
|
getLastChatsvcaggOutcome: () => ElevatedOutcome | null;
|
|
74
|
+
/**
|
|
75
|
+
* Decode-only preflight for whether the *persisted* elevated
|
|
76
|
+
* (M365ChatClient) token is present and still usable — the token the
|
|
77
|
+
* historical-version download / convert commands need. Unlike
|
|
78
|
+
* `getLastElevatedOutcome` (per-process, null in a fresh CLI invocation),
|
|
79
|
+
* this reads the on-disk cache, so a separate `deep-scan` run can tell
|
|
80
|
+
* "elevated available" from "run `login` first" without provoking a 403.
|
|
81
|
+
* Optional: only the real manager implements it; a minimal fake omits it
|
|
82
|
+
* and callers treat that as unavailable. Never captures or refreshes.
|
|
83
|
+
*/
|
|
84
|
+
getCachedElevatedInfo?: () => Promise<{
|
|
85
|
+
available: boolean;
|
|
86
|
+
expiresInSeconds: number | undefined;
|
|
87
|
+
}>;
|
|
88
|
+
/**
|
|
89
|
+
* Same decode-only preflight as `getCachedElevatedInfo`, for the chatsvcagg /
|
|
90
|
+
* ic3 Teams-chat substrate tokens. `login`'s four-token status and
|
|
91
|
+
* `scopes-check` read these; a minimal fake omits them and callers treat that
|
|
92
|
+
* as unavailable.
|
|
93
|
+
*/
|
|
94
|
+
getCachedChatsvcaggInfo?: () => Promise<{
|
|
95
|
+
available: boolean;
|
|
96
|
+
expiresInSeconds: number | undefined;
|
|
97
|
+
}>;
|
|
98
|
+
getCachedIc3Info?: () => Promise<{
|
|
99
|
+
available: boolean;
|
|
100
|
+
expiresInSeconds: number | undefined;
|
|
101
|
+
}>;
|
|
72
102
|
};
|
|
73
103
|
declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, recaptureSecondaryViaBrowser?: boolean) => AuthManager;
|
|
74
104
|
/**
|
|
@@ -148,7 +148,9 @@ type BrowserAuth = {
|
|
|
148
148
|
* failed inside the same session — caller decides whether to surface
|
|
149
149
|
* the partial success.
|
|
150
150
|
*/
|
|
151
|
-
acquireBothTokens: (teamsUrl: string
|
|
151
|
+
acquireBothTokens: (teamsUrl: string, options?: {
|
|
152
|
+
skipCacheProbe?: boolean;
|
|
153
|
+
}) => Promise<BothTokensResult>;
|
|
152
154
|
close: () => Promise<void>;
|
|
153
155
|
};
|
|
154
156
|
type ResponseLike = {
|
|
@@ -114,6 +114,32 @@ type TokenInfo = {
|
|
|
114
114
|
* worth doing under ~5 minutes) without parsing the ISO string itself.
|
|
115
115
|
*/
|
|
116
116
|
readonly expiresInSeconds: number | undefined;
|
|
117
|
+
/**
|
|
118
|
+
* Whether the *persisted* elevated (M365ChatClient) token — the one the
|
|
119
|
+
* historical-version download / convert commands need — is present and still
|
|
120
|
+
* usable, plus its raw seconds-to-expiry (`undefined` when absent). `available`
|
|
121
|
+
* is `false` when the auth manager cannot introspect it. Lets `deep-scan`
|
|
122
|
+
* preflight elevated access in a fresh process instead of turning every
|
|
123
|
+
* version download into a `403`.
|
|
124
|
+
*/
|
|
125
|
+
readonly elevated: {
|
|
126
|
+
readonly available: boolean;
|
|
127
|
+
readonly expiresInSeconds: number | undefined;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* The two Teams-chat substrate tokens (chatsvcagg / ic3), same decode-only
|
|
131
|
+
* `{ available, expiresInSeconds }` shape as `elevated`. `login` reports all four
|
|
132
|
+
* tiers so a warm session can see every token's runway; both self-heal from the
|
|
133
|
+
* shared refresh token, so they are informational rather than a preflight gate.
|
|
134
|
+
*/
|
|
135
|
+
readonly chatsvcagg: {
|
|
136
|
+
readonly available: boolean;
|
|
137
|
+
readonly expiresInSeconds: number | undefined;
|
|
138
|
+
};
|
|
139
|
+
readonly ic3: {
|
|
140
|
+
readonly available: boolean;
|
|
141
|
+
readonly expiresInSeconds: number | undefined;
|
|
142
|
+
};
|
|
117
143
|
};
|
|
118
144
|
type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
|
|
119
145
|
declare const createGraphClient: (auth: AuthManager, fetchFn?: FetchFn) => GraphClient;
|
|
@@ -29,6 +29,10 @@ declare const schema: z.ZodObject<{
|
|
|
29
29
|
true: "true";
|
|
30
30
|
false: "false";
|
|
31
31
|
}>>;
|
|
32
|
+
includeImages: z.ZodOptional<z.ZodEnum<{
|
|
33
|
+
true: "true";
|
|
34
|
+
false: "false";
|
|
35
|
+
}>>;
|
|
32
36
|
maxCells: z.ZodOptional<z.ZodString>;
|
|
33
37
|
}, z.core.$strip>;
|
|
34
38
|
declare const executeLocal: (fs: FileSystem, params: Record<string, string>) => Promise<Result<unknown, GraphError>>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Command, CommandMeta } from './command-types.js';
|
|
3
|
+
declare const schema: z.ZodObject<{
|
|
4
|
+
forwardMessageId: z.ZodString;
|
|
5
|
+
toRecipients: z.ZodString;
|
|
6
|
+
ccRecipients: z.ZodOptional<z.ZodString>;
|
|
7
|
+
bodyContent: z.ZodString;
|
|
8
|
+
subject: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
declare const execute: Command['execute'];
|
|
11
|
+
declare const meta: CommandMeta;
|
|
12
|
+
export { execute, meta, schema };
|
|
@@ -3,10 +3,6 @@ import type { Command, CommandMeta } from './command-types.js';
|
|
|
3
3
|
declare const schema: z.ZodObject<{
|
|
4
4
|
replyToMessageId: z.ZodString;
|
|
5
5
|
bodyContent: z.ZodString;
|
|
6
|
-
bodyContentType: z.ZodOptional<z.ZodEnum<{
|
|
7
|
-
Text: "Text";
|
|
8
|
-
HTML: "HTML";
|
|
9
|
-
}>>;
|
|
10
6
|
subject: z.ZodOptional<z.ZodString>;
|
|
11
7
|
}, z.core.$strip>;
|
|
12
8
|
declare const execute: Command['execute'];
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { Result } from '../../domain/result.js';
|
|
2
2
|
import type { GraphError } from '../../infra/graph-client.js';
|
|
3
|
+
import type { MediaEnvelope } from './media-files.js';
|
|
3
4
|
/**
|
|
4
5
|
* Shared by extract-drive-item-images and extract-mail-attachment-images: pick the
|
|
5
6
|
* extractor for the file's extension and run it, or return a 415 whose tail
|
|
6
7
|
* (`fetchHint`) names the caller's raw-bytes route. Both commands fetch / decode the
|
|
7
8
|
* bytes first, then hand them here, so the dispatch + media envelope live in one place.
|
|
8
9
|
*/
|
|
9
|
-
declare const extractImagesFromBytes: (bytes: Uint8Array, name: string, fetchHint: string) => Promise<Result<
|
|
10
|
+
declare const extractImagesFromBytes: (bytes: Uint8Array, name: string, fetchHint: string) => Promise<Result<MediaEnvelope, GraphError>>;
|
|
10
11
|
export { extractImagesFromBytes };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
type RefreshRoute = 'automatic' | 'interactive';
|
|
2
|
+
type TokenTier = {
|
|
3
|
+
readonly available: boolean;
|
|
4
|
+
readonly expiresInSeconds: number | undefined;
|
|
5
|
+
};
|
|
6
|
+
type TokenView = {
|
|
7
|
+
available: boolean;
|
|
8
|
+
expiresInSeconds?: number;
|
|
9
|
+
refresh: RefreshRoute;
|
|
10
|
+
reason?: string;
|
|
11
|
+
};
|
|
12
|
+
type LoginStatus = {
|
|
13
|
+
status: 'authenticated';
|
|
14
|
+
tokens: {
|
|
15
|
+
basic: TokenView;
|
|
16
|
+
elevated: TokenView;
|
|
17
|
+
chatsvcagg: TokenView;
|
|
18
|
+
ic3: TokenView;
|
|
19
|
+
};
|
|
20
|
+
hint: string;
|
|
21
|
+
};
|
|
22
|
+
type LoginStatusInput = {
|
|
23
|
+
readonly basicExpiresInSeconds: number | undefined;
|
|
24
|
+
readonly elevated: TokenTier;
|
|
25
|
+
readonly chatsvcagg: TokenTier;
|
|
26
|
+
readonly ic3: TokenTier;
|
|
27
|
+
readonly elevatedFailureReason?: string;
|
|
28
|
+
};
|
|
29
|
+
declare const buildLoginStatus: (input: LoginStatusInput) => LoginStatus;
|
|
30
|
+
export { buildLoginStatus };
|
|
31
|
+
export type { LoginStatus, LoginStatusInput };
|
|
@@ -2,5 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import type { Result } from '../../domain/result.js';
|
|
3
3
|
import type { AuthManager } from '../../infra/auth.js';
|
|
4
4
|
declare const schema: z.ZodObject<{}, z.core.$strict>;
|
|
5
|
-
declare const execute: (auth: AuthManager
|
|
5
|
+
declare const execute: (auth: AuthManager, options?: {
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}) => Promise<Result<string, import("../../infra/auth.js").AuthError>>;
|
|
6
8
|
export { execute, schema };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a comma-separated recipient string into Microsoft Graph address
|
|
3
|
+
* objects. Shared by the mail-draft write commands (create-mail-draft,
|
|
4
|
+
* update-mail-draft, create-forward-draft) so the split/trim/empty-drop
|
|
5
|
+
* behaviour stays identical across all three. Whitespace around each address
|
|
6
|
+
* is trimmed and empty segments (leading, trailing, or doubled commas) are
|
|
7
|
+
* dropped, so `"a@x.com, , b@x.com,"` yields exactly two recipients.
|
|
8
|
+
*/
|
|
9
|
+
declare const parseRecipients: (csv: string) => Array<{
|
|
10
|
+
emailAddress: {
|
|
11
|
+
address: string;
|
|
12
|
+
};
|
|
13
|
+
}>;
|
|
14
|
+
export { parseRecipients };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Result } from '../../domain/result.js';
|
|
2
2
|
import type { GraphError } from '../../infra/graph-client.js';
|
|
3
|
+
import type { MediaEnvelope } from './media-files.js';
|
|
3
4
|
/**
|
|
4
5
|
* Shared "unzip + convert every contained file" core behind
|
|
5
6
|
* `convert-drive-item-zip` (a OneDrive / SharePoint .zip),
|
|
@@ -18,6 +19,7 @@ type FileResult = {
|
|
|
18
19
|
readonly size?: number;
|
|
19
20
|
readonly text?: string;
|
|
20
21
|
readonly note?: string;
|
|
22
|
+
readonly images?: MediaEnvelope['media'];
|
|
21
23
|
};
|
|
22
24
|
type ZipArchiveResult = {
|
|
23
25
|
readonly count: number;
|
|
@@ -25,6 +27,6 @@ type ZipArchiveResult = {
|
|
|
25
27
|
readonly totalEntries?: number;
|
|
26
28
|
readonly files: ReadonlyArray<FileResult>;
|
|
27
29
|
};
|
|
28
|
-
declare const convertZipArchive: (bytes: Uint8Array, includeMetadata: boolean) => Promise<Result<ZipArchiveResult, GraphError>>;
|
|
30
|
+
declare const convertZipArchive: (bytes: Uint8Array, includeMetadata: boolean, includeImages?: boolean) => Promise<Result<ZipArchiveResult, GraphError>>;
|
|
29
31
|
export { convertZipArchive, MAX_ENTRIES };
|
|
30
32
|
export type { FileResult, ZipArchiveResult };
|
package/docs/COMMANDS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
|
|
|
@@ -126,8 +126,9 @@ For everything else:
|
|
|
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
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)` |
|
|
130
|
-
| `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)
|
|
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)` |
|
|
131
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}` |
|
|
132
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}` |
|
|
133
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}` |
|
|
@@ -251,11 +252,11 @@ For everything else:
|
|
|
251
252
|
|
|
252
253
|
| Command | Description | Required params | Graph endpoint |
|
|
253
254
|
|---------|-------------|-----------------|----------------|
|
|
254
|
-
| `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` |
|
|
255
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` |
|
|
256
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` |
|
|
257
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` |
|
|
258
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}` |
|
|
259
|
-
| `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` |
|
|
260
261
|
|
|
261
262
|
<!-- AUTO-GENERATED-COMMANDS:END -->
|
package/docs/USAGE.md
CHANGED
|
@@ -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, 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.
|
|
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": "2.
|
|
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>",
|