ask-marcel-office-cli 1.5.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/CHANGELOG.md +85 -3
  2. package/README.md +21 -58
  3. package/dist/cli.js +1620 -1753
  4. package/dist/commands.json +385 -277
  5. package/dist/composition/build-deps.d.ts +7 -8
  6. package/dist/composition/cli.d.ts +5 -4
  7. package/dist/domain/iso-datetime.d.ts +1 -1
  8. package/dist/index.js +1503 -1640
  9. package/dist/infra/auth.d.ts +6 -20
  10. package/dist/infra/browser-auth.d.ts +11 -12
  11. package/dist/infra/graph-client.d.ts +2 -2
  12. package/dist/presenter/error-hints.d.ts +9 -9
  13. package/dist/use-cases/commands/build-command.d.ts +1 -1
  14. package/dist/use-cases/commands/command-types.d.ts +5 -5
  15. package/dist/use-cases/commands/create-reply-draft.d.ts +14 -0
  16. package/dist/use-cases/commands/docs.d.ts +1 -1
  17. package/dist/use-cases/commands/get-schedule.d.ts +11 -0
  18. package/dist/use-cases/commands/graph-scopes.d.ts +1 -1
  19. package/dist/use-cases/commands/iso-datetime-schema.d.ts +1 -1
  20. package/dist/use-cases/commands/markdown-pipeline.d.ts +5 -5
  21. package/dist/use-cases/commands/option-descriptions.d.ts +1 -1
  22. package/dist/use-cases/commands/output-path.d.ts +4 -4
  23. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +1 -1
  24. package/dist/use-cases/ports/filesystem.d.ts +1 -1
  25. package/docs/COMMANDS.md +14 -12
  26. package/docs/USAGE.md +23 -23
  27. package/package.json +3 -4
  28. package/dist/composition/env.d.ts +0 -2
  29. package/dist/infra/system-browser-auth.d.ts +0 -35
  30. package/dist/infra/system-browser-loader.d.ts +0 -3
  31. package/dist/infra/token-callback-server.d.ts +0 -31
  32. package/docs/commands.json +0 -7677
@@ -6,6 +6,7 @@ import type { BrowserAuth, ElevatedFailureReason } from './browser-auth.js';
6
6
  type AuthError = {
7
7
  type: 'auth_failed';
8
8
  message: string;
9
+ code?: string;
9
10
  } | {
10
11
  type: 'auth_cancelled';
11
12
  };
@@ -15,7 +16,7 @@ type AuthError = {
15
16
  * `{ elevated: 'captured' | 'failed', elevatedReason?: ... }` field on
16
17
  * the login response so an LLM consumer can predict whether the
17
18
  * elevated-dependent commands (chat metadata, historical-version
18
- * downloads) will work without invoking them. Login-fix round-1 Wave D.
19
+ * downloads) will work without invoking them.
19
20
  */
20
21
  type ElevatedOutcome = {
21
22
  captured: true;
@@ -60,7 +61,6 @@ type AuthManager = {
60
61
  * Inspect the elevated-capture outcome from the most recent
61
62
  * `acquireViaBrowser` invocation. Returns null if no browser-acquired
62
63
  * session has happened in this process (cache hit / refresh-only).
63
- * Login-fix round-1 Wave D.
64
64
  */
65
65
  getLastElevatedOutcome: () => ElevatedOutcome | null;
66
66
  /**
@@ -70,21 +70,10 @@ type AuthManager = {
70
70
  */
71
71
  getLastChatsvcaggOutcome: () => ElevatedOutcome | null;
72
72
  };
73
- type SystemBrowserAuthFn = () => Promise<Result<{
74
- accessToken: AccessToken;
75
- refreshToken: string | null;
76
- elevatedAccessToken?: AccessToken | null;
77
- chatsvcaggAccessToken?: AccessToken | null;
78
- ic3AccessToken?: AccessToken | null;
79
- chatsvcaggRegion?: string;
80
- }, {
81
- type: string;
82
- message: string;
83
- }>>;
84
- declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, systemBrowserAuthFn?: SystemBrowserAuthFn, usePlaywrightFallback?: boolean, skipSystemBrowser?: boolean, recaptureSecondaryViaBrowser?: boolean) => AuthManager;
73
+ declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, recaptureSecondaryViaBrowser?: boolean) => AuthManager;
85
74
  /**
86
- * QA-010: probe the token cache for a fresh access token. Handed to the
87
- * browser capture so its poll loop can short-circuit the multi-minute dance
75
+ * Probe the token cache for a fresh access token. Handed to the browser
76
+ * capture so its poll loop can short-circuit the multi-minute dance
88
77
  * when a concurrent process refreshes first (AAD rotates SPA refresh tokens,
89
78
  * so the loser of the race cannot refresh and falls into the browser leg).
90
79
  * Exported for the composition test; pure read, never writes.
@@ -96,10 +85,7 @@ declare const createAuthManager: (deps: {
96
85
  logger: Logger;
97
86
  fs?: FileSystem;
98
87
  browserProfileDir?: string;
99
- systemBrowserAuth?: SystemBrowserAuthFn;
100
- usePlaywrightFallback?: boolean;
101
- skipSystemBrowser?: boolean;
102
88
  recaptureSecondaryViaBrowser?: boolean;
103
89
  }) => AuthManager;
104
90
  export { createAuthManager, createAuthManagerFromApi, createFreshCachedTokenProbe, stderrProgress };
105
- export type { AuthError, AuthManager, ElevatedOutcome, SystemBrowserAuthFn };
91
+ export type { AuthError, AuthManager, ElevatedOutcome };
@@ -13,7 +13,7 @@ type BrowserTokenResult = {
13
13
  * one retry after wiping the profile; `navigation_failed` is a network
14
14
  * issue, not worth retrying).
15
15
  *
16
- * Login-fix round-1: was previously `AccessToken | null`, which conflated
16
+ * was previously `AccessToken | null`, which conflated
17
17
  * "browser launch hung", "navigation broke", and "silent-SSO polling
18
18
  * timed out" into a single null and made the error message inaccurate.
19
19
  */
@@ -69,7 +69,7 @@ type Ic3TokenResult = {
69
69
  /**
70
70
  * Combined outcome of capturing all four tokens (Teams basic / M365
71
71
  * elevated / chatsvcagg substrate / IC3 substrate) inside one browser
72
- * session. Login-fix round-2 introduced this for the basic+elevated
72
+ * session. introduced this for the basic+elevated
73
73
  * pair; chatsvcagg was added next; IC3 is the most recent leg, capturing
74
74
  * the bearer Teams web uses for unbounded chat-history reads.
75
75
  *
@@ -84,7 +84,7 @@ type BothTokensResult = {
84
84
  readonly ic3: Ic3TokenResult;
85
85
  /**
86
86
  * Set when the Teams poll short-circuited because `freshCachedToken`
87
- * found a token written by a concurrent process (QA-010). The caller
87
+ * found a token written by a concurrent process. The caller
88
88
  * must NOT persist this result — the cache is already the source of
89
89
  * truth, and `refreshToken` is null here (persisting would clobber the
90
90
  * winner's rotated refresh token).
@@ -92,7 +92,6 @@ type BothTokensResult = {
92
92
  readonly fromCache?: true;
93
93
  };
94
94
  type BrowserAuth = {
95
- acquireToken: (scopes: string[], startUrl: string) => Promise<BrowserTokenResult | null>;
96
95
  /**
97
96
  * Capture an "elevated" Graph access token by navigating to a
98
97
  * different Microsoft web app whose first-party app identity is on
@@ -136,11 +135,11 @@ type BrowserAuth = {
136
135
  */
137
136
  acquireIc3Token: () => Promise<Ic3TokenResult>;
138
137
  /**
139
- * Login-fix round-2: capture BOTH tokens inside ONE browser session.
138
+ * capture BOTH tokens inside ONE browser session.
140
139
  * After the Teams response listener intercepts the Teams token,
141
140
  * navigate the SAME page to the elevated URL and harvest the
142
141
  * M365ChatClient bearer from outgoing request headers. Cookies are
143
- * live in memory, so federated SSO chains (e.g. Okta-fronted
142
+ * live in memory, so federated SSO chains (e.g. third-party-IdP-fronted
144
143
  * tenants) work without a second visible sign-in.
145
144
  *
146
145
  * Returns `teams: null` if no Teams token came back within the full
@@ -149,7 +148,7 @@ type BrowserAuth = {
149
148
  * failed inside the same session — caller decides whether to surface
150
149
  * the partial success.
151
150
  */
152
- acquireBothTokens: (scopes: string[], teamsUrl: string) => Promise<BothTokensResult>;
151
+ acquireBothTokens: (teamsUrl: string) => Promise<BothTokensResult>;
153
152
  close: () => Promise<void>;
154
153
  };
155
154
  type ResponseLike = {
@@ -199,7 +198,7 @@ type BrowserAuthConfig = {
199
198
  readonly fs: FileSystem;
200
199
  readonly trace?: TraceFn;
201
200
  /**
202
- * QA-010 (login hang): probe for a fresh token that landed in the cache
201
+ * (login hang): probe for a fresh token that landed in the cache
203
202
  * WHILE the browser capture is polling. AAD SPA refresh tokens rotate, so
204
203
  * when two processes race, the loser's refresh fails and it falls into the
205
204
  * full multi-minute browser dance — while the winner's fresh token sits in
@@ -210,7 +209,7 @@ type BrowserAuthConfig = {
210
209
  /**
211
210
  * User-visible progress sink (stderr in production). The browser capture
212
211
  * can legitimately take minutes (federated SSO, interactive sign-in); these
213
- * one-liners are what separates "waiting on the user" from "hung" (QA-010).
212
+ * one-liners are what separates "waiting on the user" from "hung".
214
213
  */
215
214
  readonly onProgress?: (line: string) => void;
216
215
  readonly profileDir?: string;
@@ -222,19 +221,19 @@ type BrowserAuthConfig = {
222
221
  /**
223
222
  * Deadline for the SILENT elevated-token recapture flow (no user
224
223
  * interaction expected — persistent profile cookies do the SSO).
225
- * Defaults to 20s. The audit (v1.0.0 §1.1) flagged that reusing the
224
+ * Defaults to 20s. The audit () flagged that reusing the
226
225
  * 5-minute interactive `pollDeadlineMs` for this silent path made
227
226
  * `list-chats` etc. hang for minutes when cookies were stale, blowing
228
227
  * the LLM tool-call window. With a tight cap, the flow either yields
229
228
  * a token quickly or fails with `auth_failed: elevated token capture
230
- * timed out — run `ask-marcel login` to refresh.`
229
+ * timed out — run `ask-marcel-office login` to refresh.`
231
230
  */
232
231
  readonly elevatedRecaptureTimeoutMs?: number;
233
232
  /**
234
233
  * Hard deadline on `launchPersistentContext` + `newPage` for the
235
234
  * elevated capture path. Defaults to 15s. Distinct from
236
235
  * `elevatedRecaptureTimeoutMs` so the error message can name which
237
- * step hung — launch vs polling. Audit login-fix round-1: previously
236
+ * step hung — launch vs polling. Audit previously
238
237
  * unguarded, so a hung Playwright launch (corrupt persistent profile
239
238
  * with stale `Singleton*` locks, or a slow browser binary) would
240
239
  * block the whole command indefinitely.
@@ -109,8 +109,8 @@ type TokenInfo = {
109
109
  /**
110
110
  * Seconds remaining until the cached token's `exp` claim — derived from
111
111
  * `expiresAt - now`. Negative when the token has already expired. Absent
112
- * when the JWT did not carry an `exp` claim. Audit Jane-session §4: lets
113
- * an LLM decide pre-emptively to run `ask-marcel login` (re-auth typically
112
+ * when the JWT did not carry an `exp` claim. lets
113
+ * an LLM decide pre-emptively to run `ask-marcel-office login` (re-auth typically
114
114
  * worth doing under ~5 minutes) without parsing the ISO string itself.
115
115
  */
116
116
  readonly expiresInSeconds: number | undefined;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Translate Graph / substrate / CLI / validation errors into actionable hints.
3
3
  *
4
- * Audit Jane-session §2: bare `error: ErrorInvalidIdMalformed: Id is
4
+ * bare `error: ErrorInvalidIdMalformed: Id is
5
5
  * malformed.` had no remedy for the LLM — it had to guess where the bad ID
6
6
  * came from. This module is the centralised "what should I do about this"
7
7
  * lookup: pattern-match the error code (or, as a fallback, a substring of
@@ -12,14 +12,14 @@
12
12
  * `--output text` (as `hint:` / `source:` lines under the existing `error:`
13
13
  * line).
14
14
  *
15
- * Audit Jane-session §2 follow-up: the four error-envelope variants are
16
- * - `graph` — public Microsoft Graph API at /v1.0/
17
- * - `substrate` — Microsoft-internal chat substrates (chatsvcagg / IC3).
18
- * Tagged at the infra layer with `substrateHttp{N}_{name}`.
19
- * - `cli` — CLI itself (Commander parser, CLI rewrites of Graph
20
- * errors via `cli_rewrite_*` and `cli_reject_*` codes)
21
- * - `validation` — Zod schema validation from use-cases (no `code` — pure
22
- * message-pattern fallback)
15
+ * the four error-envelope variants are
16
+ * - `graph` — public Microsoft Graph API at /v1.0/
17
+ * - `substrate` — Microsoft-internal chat substrates (chatsvcagg / IC3).
18
+ * Tagged at the infra layer with `substrateHttp{N}_{name}`.
19
+ * - `cli` — CLI itself (Commander parser, CLI rewrites of Graph
20
+ * errors via `cli_rewrite_*` and `cli_reject_*` codes)
21
+ * - `validation` — Zod schema validation from use-cases (no `code` — pure
22
+ * message-pattern fallback)
23
23
  *
24
24
  * Rule precedence: specific code matchers run FIRST, then message-pattern
25
25
  * fallbacks. The generic-validation rule sits LAST so it never overrides a
@@ -7,7 +7,7 @@ import { type ODataKey } from './odata-query.js';
7
7
  * passthroughs at all). `defaultSelect`, when set and the user did NOT pass
8
8
  * `--select`, is injected into the OData query string so default invocations
9
9
  * return a slim projection instead of a 50 KB resource. User-supplied
10
- * `--select` always wins. Audit Jane-session §A: pairs the `list-mail-attachments`
10
+ * `--select` always wins. pairs the `list-mail-attachments`
11
11
  * pattern with the builder layer so the 6 heaviest endpoints stop returning
12
12
  * the full Graph resource by default.
13
13
  */
@@ -52,7 +52,7 @@ type CommandOptionMeta = {
52
52
  };
53
53
  /**
54
54
  * A positional argument (i.e. NOT a `--flag`). Used today only for the
55
- * `docs` lifecycle command (`ask-marcel docs <command>`) but kept as its
55
+ * `docs` lifecycle command (`ask-marcel-office docs <command>`) but kept as its
56
56
  * own field so the manifest never claims a positional is a flag. An LLM
57
57
  * consumer reading `help-json` can branch on the presence of
58
58
  * `positionalArguments` to know to skip the `--` prefix.
@@ -100,7 +100,7 @@ type CommandMeta = {
100
100
  readonly paginationStrategy?: PaginationStrategy;
101
101
  /**
102
102
  * Graph permission scopes the endpoint requires. The basic Teams web-client
103
- * token grants ~30 scopes (run `ask-marcel scopes-check` to see). Commands
103
+ * token grants ~30 scopes (run `ask-marcel-office scopes-check` to see). Commands
104
104
  * with unmet scopes return `403 Forbidden: Missing scope` at the wire. Use
105
105
  * this for pre-flight checks rather than failing on-the-wire. Optional —
106
106
  * populated only on commands where the audit confirmed a scope-failure
@@ -120,7 +120,7 @@ type CommandMeta = {
120
120
  * captured at login from `teams.microsoft.com`) — the Teams chat-content
121
121
  * commands. Like `needsElevatedToken`, an LLM should check this before
122
122
  * invoking and warm up an interactive `login`; a headless or stale session
123
- * times out on these (the non-interactive silent-SSO limitation, QA-011).
123
+ * times out on these (the non-interactive silent-SSO limitation, ).
124
124
  */
125
125
  readonly needsSubstrateToken?: true;
126
126
  /**
@@ -128,7 +128,7 @@ type CommandMeta = {
128
128
  * or `{contentType, size, text}`) and is therefore a valid target for the
129
129
  * global `--output-path` flag. Used by the CLI composition to derive the
130
130
  * rejection-message whitelist from the manifest rather than hand-keeping it
131
- * as a string literal. Audit round-8 Wave E2.
131
+ * as a string literal. Audit .
132
132
  */
133
133
  readonly producesBytes?: true;
134
134
  /**
@@ -157,7 +157,7 @@ type CommandMeta = {
157
157
  * substrate (chatsvcagg / IC3) that is not in the public Graph API and can
158
158
  * break on a Teams web-client update — the docstring "Best-effort, may break
159
159
  * on Microsoft client updates" warnings now have a structured pair.
160
- * Audit Jane-session §6.
160
+ *
161
161
  */
162
162
  readonly stability?: 'experimental';
163
163
  };
@@ -0,0 +1,14 @@
1
+ import { z } from 'zod';
2
+ import type { Command, CommandMeta } from './command-types.js';
3
+ declare const schema: z.ZodObject<{
4
+ replyToMessageId: z.ZodString;
5
+ bodyContent: z.ZodString;
6
+ bodyContentType: z.ZodOptional<z.ZodEnum<{
7
+ Text: "Text";
8
+ HTML: "HTML";
9
+ }>>;
10
+ subject: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>;
12
+ declare const execute: Command['execute'];
13
+ declare const meta: CommandMeta;
14
+ export { execute, meta, schema };
@@ -13,7 +13,7 @@ export type DocsError = {
13
13
  * `scopesRequired` — everything the LLM only needs once it's already decided
14
14
  * to invoke. `stability` is kept (it's a discovery-time concern: LLMs prefer
15
15
  * stable siblings when they exist, so they need to see the tag at discovery
16
- * time, not after a second full-manifest fetch). Audit Jane-session §B/§6.
16
+ * time, not after a second full-manifest fetch). /§6.
17
17
  */
18
18
  export type TerseManifestEntry = {
19
19
  readonly name: string;
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ import type { Command, CommandMeta } from './command-types.js';
3
+ declare const schema: z.ZodObject<{
4
+ schedules: z.ZodString;
5
+ startDateTime: z.ZodPipe<z.ZodString, z.ZodTransform<import("../../domain/iso-datetime.js").IsoDateTime, string>>;
6
+ endDateTime: z.ZodPipe<z.ZodString, z.ZodTransform<import("../../domain/iso-datetime.js").IsoDateTime, string>>;
7
+ availabilityViewInterval: z.ZodOptional<z.ZodString>;
8
+ }, z.core.$strip>;
9
+ declare const execute: Command['execute'];
10
+ declare const meta: CommandMeta;
11
+ export { execute, meta, schema };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Central Microsoft Graph permission scope map (audit round-8 Wave C).
2
+ * Central Microsoft Graph permission scope map.
3
3
  *
4
4
  * Source of truth: Microsoft Graph permissions reference
5
5
  * (https://learn.microsoft.com/en-us/graph/permissions-reference). Each
@@ -6,7 +6,7 @@ import { z } from 'zod';
6
6
  * compose this into their schemas in place of `z.string().min(1)`; the
7
7
  * URL-builder sees the already-resolved canonical ISO form.
8
8
  *
9
- * Audit Jane-session §C: removes the manual ISO arithmetic an LLM had to do
9
+ * removes the manual ISO arithmetic an LLM had to do
10
10
  * to ask "what changed this week" against `list-calendar-view`. Type
11
11
  * inference is left to Zod so the calendar commands' shape signatures stay
12
12
  * compatible with `buildListCommand`'s `ZodRawShape` constraint.
@@ -5,11 +5,11 @@ import { type InlineAttachment } from './inline-image-embedder.js';
5
5
  * Orchestrate the four steps that turn a Graph `?format=html` content
6
6
  * call into a markdown envelope:
7
7
  *
8
- * 1. getBinary(contentPath)
9
- * 2. if Graph returned a 302 downloadUrl, follow via fetchUrl (host
10
- * allow-list enforced inside fetchUrl, Hardening #3)
11
- * 3. embedInlineImages over any cid: refs (Hardening #1: image/* only)
12
- * 4. htmlToMarkdown via turndown
8
+ * 1. getBinary(contentPath)
9
+ * 2. if Graph returned a 302 downloadUrl, follow via fetchUrl (host
10
+ * allow-list enforced inside fetchUrl, Hardening #3)
11
+ * 3. embedInlineImages over any cid: refs (Hardening #1: image/* only)
12
+ * 4. htmlToMarkdown via turndown
13
13
  *
14
14
  * Returns `{ contentType: 'text/markdown', size, text }` on success.
15
15
  */
@@ -2,4 +2,4 @@
2
2
  * `--drive-id` description for every generic OneDrive / SharePoint drive-item
3
3
  * command. Always points the caller at how to obtain a drive id.
4
4
  */
5
- export declare const DRIVE_ID_DESCRIPTION = "Microsoft Graph drive ID. Use `ask-marcel list-drives` for the personal OneDrive, or `ask-marcel list-sharepoint-site-drives --site-id <id>` for a SharePoint document library.";
5
+ export declare const DRIVE_ID_DESCRIPTION = "Microsoft Graph drive ID. Use `ask-marcel-office list-drives` for the personal OneDrive, or `ask-marcel-office list-sharepoint-site-drives --site-id <id>` for a SharePoint document library.";
@@ -11,10 +11,10 @@ import type { FileSystem } from '../ports/filesystem.js';
11
11
  * Two recognized inline shapes — both produced by `inlineBinary` and
12
12
  * `office-to-markdown`:
13
13
  *
14
- * 1. `{ contentType, size, base64 }` — written via `fs.writeBytes`,
15
- * with `base64` replaced by `savedTo`.
16
- * 2. `{ contentType, size, text }` — written via `fs.writeText`,
17
- * with `text` replaced by `savedTo`.
14
+ * 1. `{ contentType, size, base64 }` — written via `fs.writeBytes`,
15
+ * with `base64` replaced by `savedTo`.
16
+ * 2. `{ contentType, size, text }` — written via `fs.writeText`,
17
+ * with `text` replaced by `savedTo`.
18
18
  *
19
19
  * Anything else (plain JSON gets, error envelopes, etc.) returns
20
20
  * `no_inlined_bytes` so the CLI can surface a clear error rather than
@@ -8,7 +8,7 @@ import type { GraphError } from '../../infra/graph-client.js';
8
8
  * `bytesToMarkdown` dispatch the markdown commands use; an entry the dispatch
9
9
  * can't convert (image, binary, nested archive, scanned PDF) is LISTED with a
10
10
  * note instead of failing the whole archive. Notes use the container-neutral
11
- * NESTED_HINTS (QA-007): entries live INSIDE the zip, so caller-specific
11
+ * NESTED_HINTS: entries live INSIDE the zip, so caller-specific
12
12
  * sibling-command pointers (`extract-drive-item-images`, …) cannot reach them.
13
13
  */
14
14
  declare const MAX_ENTRIES = 100;
@@ -19,7 +19,7 @@ export type FileSystem = {
19
19
  readonly writeBytes: (path: string, bytes: Uint8Array) => Promise<Result<void, FileSystemError>>;
20
20
  /**
21
21
  * Restrict a file's permission bits (e.g. 0o600 on the token cache so
22
- * other local users cannot read cached secrets — QA-001).
22
+ * other local users cannot read cached secrets — ).
23
23
  */
24
24
  readonly chmod: (path: string, mode: number) => Promise<Result<void, FileSystemError>>;
25
25
  readonly deleteIfExists: (path: string) => Promise<Result<void, FileSystemError>>;
package/docs/COMMANDS.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # Command reference
2
2
 
3
- All 176 commands across 12 categories, grouped by category. Each row shows the command name, a one-line summary, the required parameters, and the underlying Microsoft Graph endpoint.
3
+ All 179 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. Audit v1.0.0 §D4 caveat for `--format pdf`: Graph sometimes silently falls back to raw source bytes for the current version (which Graph occasionally serves through this endpoint) — when the response carries `passthrough: true`, save with the source extension, not `.pdf` (the global output-path flag refuses the mismatch). | `--drive-id`, `--item-id`, `--version-id`, `--format`, `--include-metadata` | `GET /drives/{drive-id}/items/{item-id}/versions/{version-id}/content` |
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,9 @@ 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 (audit v1.0.0): one call for the body, one for the attachments-metadata list (only if `hasAttachments:true`), and one per small inline image — replaces the old `?$expand=attachments` which timed out / truncated on messages with multi-MB attachments. | `--message-id`, `--inline-images`, `--keep-quoted` | `GET /me/messages/{message-id}` |
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
129
  | `create-mail-draft` | Create a new mail draft. POST /me/messages (or /me/mailFolders/{id}/messages when --mail-folder-id is set). The draft is saved in the Drafts folder (or the specified folder) and can be sent later via the Outlook client or Graph sendMail. Recipients are comma-separated email addresses. Returns the created message object with its id — use this id with update-mail-draft to modify the draft before sending. | `--subject`, `--body-content`, `--body-content-type`, `--to-recipients`, `--cc-recipients`, `--bcc-recipients`, `--importance`, `--mail-folder-id` | `POST /me/messages (or /me/mailFolders/{mail-folder-id}/messages)` |
130
+ | `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), then PATCH places the reply body above the quote. 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`, `--body-content-type`, `--subject` | `POST /me/messages/{reply-to-message-id}/createReplyAll (then PATCH the returned draft)` |
130
131
  | `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
132
  | `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
133
  | `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 +180,7 @@ For everything else:
179
180
  | `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
181
  | `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
182
  | `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[]` (audit v1.0.0 §D7 — even though only one tenant exists, the endpoint returns a collection). The full resource is ~57 KB; use `--select` to slim it (e.g. `--select id,displayName,verifiedDomains`). | `--select`, `--expand` | `GET /organization` |
183
+ | `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
184
  | `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
185
  | `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
186
  | `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 +200,7 @@ For everything else:
199
200
  | `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
201
  | `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
202
  | `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` |
203
+ | `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
204
  | `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
205
  | `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
206
  | `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 +225,7 @@ For everything else:
223
225
 
224
226
  | Command | Description | Required params | Graph endpoint |
225
227
  |---------|-------------|-----------------|----------------|
226
- | `find-chats-with-user` | Find every Microsoft Teams chat that includes a member matching `--name` (substring search across display-name, email, given-name, surname, MRI, and object-id). Both sides are Unicode-folded (NFD + combining-mark strip) and lowercased before comparison, so `--name Jane` matches `Jane DOE` AND `jane.doe@example.com` AND `JANE` — important because a dual-identity user often carries the accented display-name on one identity and the un-accented email on the other. Walks the paginated chat-list substrate up to `--max-pages` and returns matching chats with their `matchedMembers[]`. Collapses the canonical "all conversations with person X" workflow into a single call AND surfaces dual-identity people (e.g. someone with both an org MRI and a guest-tenant MRI). **Best-effort, may break on Microsoft client updates** — the chat substrate is not in the public Microsoft Graph API. | `--name`, `--max-pages`, `--page-size` | `GET https://teams.microsoft.com/api/csa/{region}/api/v3/teams/users/me/chats` |
228
+ | `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
229
  | `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
230
  | `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
231
  | `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` |
@@ -251,9 +253,9 @@ For everything else:
251
253
  |---------|-------------|-----------------|----------------|
252
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` |
253
255
  | `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). Audit Jane-session §5.2: `tenantTimeZone` lets an LLM stop treating every datetime as UTC on first contact. | _(none)_ | `GET (meta) parallel: /me, /me/drive, /me/mailFolders/inbox, /me/calendar, /me/planner/plans, /me/onenote/notebooks, /me/joinedTeams, /me/drive/recent, /me/mailboxSettings` |
256
+ | `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
+ | `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
258
  | `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 (added Jane-session §4) lets an LLM decide pre-emptively to `login` again — typically worth doing under ~5 minutes (300 s) so a long-running session doesn't hit the wall mid-command. | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
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 (added ) lets an LLM decide pre-emptively to `login` again — typically worth doing under ~5 minutes (300 s) so a long-running session doesn't hit the wall mid-command. | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
258
260
 
259
261
  <!-- AUTO-GENERATED-COMMANDS:END -->