ask-marcel-office-cli 2.3.0 → 2.4.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 +121 -0
- package/README.md +45 -26
- package/dist/cli.js +2819 -2207
- package/dist/commands.json +186 -12
- package/dist/composition/run-registry-command.d.ts +4 -4
- package/dist/domain/utilities/base64.d.ts +11 -0
- package/dist/index.js +1983 -1536
- package/dist/infra/auth.d.ts +30 -4
- package/dist/infra/browser-auth.d.ts +21 -1
- package/dist/presenter/output-text.d.ts +1 -1
- package/dist/presenter/output.d.ts +2 -2
- package/dist/presenter/render-to-string.d.ts +14 -6
- package/dist/use-cases/commands/docx-metadata.d.ts +32 -2
- package/dist/use-cases/commands/include-hidden-folders.d.ts +2 -0
- package/dist/use-cases/commands/list-shared-mailbox-child-folders.d.ts +4 -0
- package/dist/use-cases/commands/list-shared-mailbox-folders.d.ts +4 -0
- package/dist/use-cases/commands/login-status.d.ts +2 -0
- package/dist/use-cases/commands/login.d.ts +18 -8
- package/dist/use-cases/commands/next-page.d.ts +1 -0
- package/dist/use-cases/commands/ooxml-xml-walker.d.ts +31 -6
- package/dist/use-cases/commands/pptx-comments.d.ts +8 -5
- package/dist/use-cases/commands/token-tier-capability.d.ts +5 -0
- package/docs/COMMANDS.md +8 -6
- package/docs/USAGE.md +2 -2
- package/package.json +25 -9
package/dist/infra/auth.d.ts
CHANGED
|
@@ -39,7 +39,9 @@ type AuthManager = {
|
|
|
39
39
|
* `logicalPermissions` allow-list. Falls through cache → re-capture
|
|
40
40
|
* via headless Playwright. Used by the 3 historical-version commands.
|
|
41
41
|
*/
|
|
42
|
-
getElevatedAccessToken: (
|
|
42
|
+
getElevatedAccessToken: (options?: {
|
|
43
|
+
readonly awaitSignIn?: boolean;
|
|
44
|
+
}) => Promise<Result<AccessToken, AuthError>>;
|
|
43
45
|
/**
|
|
44
46
|
* Returns a Graph token issued by a PARTNER tenant's authority, for a user
|
|
45
47
|
* who is a guest there. Without it, every call against that tenant's
|
|
@@ -54,7 +56,9 @@ type AuthManager = {
|
|
|
54
56
|
* through cache → re-capture via headless Playwright. Used by the
|
|
55
57
|
* `list-teams-chats-with-messages` family of commands.
|
|
56
58
|
*/
|
|
57
|
-
getChatsvcaggAccessToken: (
|
|
59
|
+
getChatsvcaggAccessToken: (options?: {
|
|
60
|
+
readonly ignoreCache?: boolean;
|
|
61
|
+
}) => Promise<Result<AccessToken, AuthError>>;
|
|
58
62
|
/**
|
|
59
63
|
* Returns the regional segment used to construct chatsvcagg substrate
|
|
60
64
|
* URLs (`teams.microsoft.com/api/csa/<region>/api/...`). Captured at
|
|
@@ -71,7 +75,17 @@ type AuthManager = {
|
|
|
71
75
|
* via headless Playwright. Used by `list-teams-chat-history` to walk
|
|
72
76
|
* paginated chat-message history beyond the 200-message chatsvcagg cap.
|
|
73
77
|
*/
|
|
74
|
-
getIc3AccessToken: (
|
|
78
|
+
getIc3AccessToken: (options?: {
|
|
79
|
+
readonly ignoreCache?: boolean;
|
|
80
|
+
}) => Promise<Result<AccessToken, AuthError>>;
|
|
81
|
+
/**
|
|
82
|
+
* Redeem the shared refresh token for any COLD substrate tier, over HTTP, with
|
|
83
|
+
* no browser on any path. `login` calls this so a warm-cache sign-in leaves all
|
|
84
|
+
* four tiers usable; without it the two substrate tiers stay cold until some
|
|
85
|
+
* Teams-chat command pays for them, and `login` reports them missing while
|
|
86
|
+
* having done nothing about it.
|
|
87
|
+
*/
|
|
88
|
+
warmSubstrateTokens?: () => Promise<void>;
|
|
75
89
|
logout: () => Promise<Result<void, AuthError>>;
|
|
76
90
|
/**
|
|
77
91
|
* Inspect the elevated-capture outcome from the most recent
|
|
@@ -95,6 +109,16 @@ type AuthManager = {
|
|
|
95
109
|
* Optional: only the real manager implements it; a minimal fake omits it
|
|
96
110
|
* and callers treat that as unavailable. Never captures or refreshes.
|
|
97
111
|
*/
|
|
112
|
+
/**
|
|
113
|
+
* The cached BASIC token, decoded by `scopes-check` and never acquired: no
|
|
114
|
+
* refresh, no browser. The acquiring getter heals a dead session by opening
|
|
115
|
+
* a browser and, when the persistent profile is already signed in, wiping it
|
|
116
|
+
* so the grant re-fires (2026-09-02: a diagnostic did exactly that). A stale
|
|
117
|
+
* token comes back as-is so its expiry can be reported; no cache at all is
|
|
118
|
+
* `undefined`. Optional: a bring-your-own-token manager omits it and callers
|
|
119
|
+
* fall back to `getAccessToken`, which is then the caller's own function.
|
|
120
|
+
*/
|
|
121
|
+
getCachedBasicToken?: () => Promise<AccessToken | undefined>;
|
|
98
122
|
getCachedElevatedInfo?: () => Promise<CachedTierInfo>;
|
|
99
123
|
/**
|
|
100
124
|
* Same decode-only preflight as `getCachedElevatedInfo`, for the chatsvcagg /
|
|
@@ -120,7 +144,8 @@ type SecondaryTokenCommands = {
|
|
|
120
144
|
readonly chatsvcagg: ReadonlyArray<string>;
|
|
121
145
|
readonly ic3: ReadonlyArray<string>;
|
|
122
146
|
};
|
|
123
|
-
|
|
147
|
+
type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
|
|
148
|
+
declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, recaptureSecondaryViaBrowser?: boolean, secondaryTokenCommands?: SecondaryTokenCommands, acquireBasicViaBrowser?: boolean, recaptureElevatedViaBrowser?: boolean, fetchFn?: FetchFn) => AuthManager;
|
|
124
149
|
/**
|
|
125
150
|
* Probe the token cache for a fresh access token. Handed to the browser
|
|
126
151
|
* capture so its poll loop can short-circuit the multi-minute dance
|
|
@@ -138,6 +163,7 @@ declare const createAuthManager: (deps: {
|
|
|
138
163
|
recaptureSecondaryViaBrowser?: boolean;
|
|
139
164
|
secondaryTokenCommands?: SecondaryTokenCommands;
|
|
140
165
|
acquireBasicViaBrowser?: boolean;
|
|
166
|
+
recaptureElevatedViaBrowser?: boolean;
|
|
141
167
|
}) => AuthManager;
|
|
142
168
|
export { createAuthManager, createAuthManagerFromApi, createFreshCachedTokenProbe, stderrProgress };
|
|
143
169
|
export type { AuthError, AuthManager, CachedTierInfo, ElevatedOutcome, SecondaryTokenCommands };
|
|
@@ -115,8 +115,16 @@ type BrowserAuth = {
|
|
|
115
115
|
* cached elevated token expires and the Teams session is already in
|
|
116
116
|
* the cache (no fresh sign-in needed). On `login`, use
|
|
117
117
|
* `acquireBothTokens` instead so the user sees only one browser.
|
|
118
|
+
*
|
|
119
|
+
* `awaitSignIn` says a human asked for this and is watching, so the capture
|
|
120
|
+
* gets the interactive `pollDeadlineMs` rather than the short silent cap:
|
|
121
|
+
* long enough for a slow redirect chain, and long enough to fill in a
|
|
122
|
+
* sign-in form if one appears. Without it the short cap applies and the
|
|
123
|
+
* capture fails fast, which is what a background command needs.
|
|
118
124
|
*/
|
|
119
|
-
acquireElevatedToken: (
|
|
125
|
+
acquireElevatedToken: (options?: {
|
|
126
|
+
readonly awaitSignIn?: boolean;
|
|
127
|
+
}) => Promise<ElevatedTokenResult>;
|
|
120
128
|
/**
|
|
121
129
|
* Capture a chatsvcagg-audience bearer (Teams basic identity, but the
|
|
122
130
|
* `chatsvcagg.teams.microsoft.com` resource instead of Graph) by
|
|
@@ -223,6 +231,12 @@ type BrowserAuthConfig = {
|
|
|
223
231
|
readonly initialSettleMs?: number;
|
|
224
232
|
readonly postReloginSettleMs?: number;
|
|
225
233
|
readonly pollIntervalMs?: number;
|
|
234
|
+
/**
|
|
235
|
+
* How long to keep listening for the PREFERRED elevated identity after an
|
|
236
|
+
* acceptable-but-weaker one has already been captured. See
|
|
237
|
+
* `PREFERRED_ELEVATED_APP_ID`.
|
|
238
|
+
*/
|
|
239
|
+
readonly elevatedPreferenceGraceMs?: number;
|
|
226
240
|
readonly pollDeadlineMs?: number;
|
|
227
241
|
readonly navigationTimeoutMs?: number;
|
|
228
242
|
/**
|
|
@@ -234,6 +248,12 @@ type BrowserAuthConfig = {
|
|
|
234
248
|
* the LLM tool-call window. With a tight cap, the flow either yields
|
|
235
249
|
* a token quickly or fails with `auth_failed: elevated token capture
|
|
236
250
|
* timed out — run `ask-marcel-office login` to refresh.`
|
|
251
|
+
*
|
|
252
|
+
* The cap applies only when nobody is waiting. A caller passing
|
|
253
|
+
* `awaitSignIn` (today just `login`) gets `pollDeadlineMs` instead, because
|
|
254
|
+
* a silent capture that merely runs SLOW would otherwise fail here and send
|
|
255
|
+
* `login` into the forced dance, whose cookie wipe costs the 90-day sign-in
|
|
256
|
+
* session and guarantees the next run prompts.
|
|
237
257
|
*/
|
|
238
258
|
readonly elevatedRecaptureTimeoutMs?: number;
|
|
239
259
|
/**
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const renderTextOutput: (data: unknown) => string;
|
|
1
|
+
declare const renderTextOutput: (data: unknown, tenantId?: string) => string;
|
|
2
2
|
export { renderTextOutput };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from '../use-cases/ports/logger.js';
|
|
2
2
|
import type { ErrorSource } from './error-hints.js';
|
|
3
|
-
import type { OutputFormat,
|
|
4
|
-
declare const render: (data: unknown, logger: Logger, format: OutputFormat, context?:
|
|
3
|
+
import type { OutputFormat, RenderContext } from './render-to-string.js';
|
|
4
|
+
declare const render: (data: unknown, logger: Logger, format: OutputFormat, context?: RenderContext) => void;
|
|
5
5
|
declare const renderError: (message: string, format: OutputFormat, errorCode?: string, explicitSource?: ErrorSource, retryAfterSeconds?: number) => void;
|
|
6
6
|
export { render, renderError };
|
|
7
7
|
export type { OutputFormat };
|
|
@@ -9,9 +9,10 @@ type OutputFormat = 'text' | 'json';
|
|
|
9
9
|
type RenderSurface = 'cli' | 'mcp';
|
|
10
10
|
/**
|
|
11
11
|
* What the presenter needs in order to name a remedy the caller can actually
|
|
12
|
-
* use
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* use, and to render a follow-up command the caller can actually run. Built in
|
|
13
|
+
* composition (which owns the command manifest AND the invoked params); omitted
|
|
14
|
+
* by the renders that have no command behind them at all — the 548 KB
|
|
15
|
+
* `list-commands` manifest, a login summary, a logout status.
|
|
15
16
|
*
|
|
16
17
|
* 2026-07-23: the banner used to call `--output-path` a "universal remedy
|
|
17
18
|
* (works on every command)". It is not. Plain-JSON commands REFUSE the flag,
|
|
@@ -20,13 +21,20 @@ type RenderSurface = 'cli' | 'mcp';
|
|
|
20
21
|
* banner named was a dead end there, and an agentic caller following it burned
|
|
21
22
|
* a call to find out.
|
|
22
23
|
*/
|
|
23
|
-
type
|
|
24
|
+
type RenderContext = {
|
|
24
25
|
readonly commandName: string;
|
|
25
26
|
/** `meta.producesBytes` — the manifest flag that decides whether --output-path is accepted. */
|
|
26
27
|
readonly producesBytes: boolean;
|
|
27
28
|
readonly supportsSelect: boolean;
|
|
28
29
|
readonly supportsTop: boolean;
|
|
29
30
|
readonly surface: RenderSurface;
|
|
31
|
+
/**
|
|
32
|
+
* The `--tenant-id` the call was made with, when it carried one. Unlike every
|
|
33
|
+
* field above it comes from the INVOCATION, not the manifest: a Graph cursor
|
|
34
|
+
* carries no tenant, so the footer is the only place the next call can learn
|
|
35
|
+
* which identity signed this page.
|
|
36
|
+
*/
|
|
37
|
+
readonly tenantId?: string;
|
|
30
38
|
};
|
|
31
39
|
/**
|
|
32
40
|
* Render a use-case success value to its final string, newline included.
|
|
@@ -36,7 +44,7 @@ type SizeHintContext = {
|
|
|
36
44
|
* whenever a registry command produced the data. Omit it for the renders that
|
|
37
45
|
* have no command behind them; the banner then claims no flag-level remedy.
|
|
38
46
|
*/
|
|
39
|
-
declare const renderToString: (data: unknown, format: OutputFormat, context?:
|
|
47
|
+
declare const renderToString: (data: unknown, format: OutputFormat, context?: RenderContext) => string;
|
|
40
48
|
/**
|
|
41
49
|
* Render an error to its final string, newline included. The `hint` / `source`
|
|
42
50
|
* lookup is shared with the CLI so an MCP consumer sees the same curated
|
|
@@ -44,4 +52,4 @@ declare const renderToString: (data: unknown, format: OutputFormat, context?: Si
|
|
|
44
52
|
*/
|
|
45
53
|
declare const renderErrorToString: (message: string, format: OutputFormat, errorCode?: string, explicitSource?: ErrorSource, retryAfterSeconds?: number) => string;
|
|
46
54
|
export { renderErrorToString, renderToString };
|
|
47
|
-
export type { OutputFormat,
|
|
55
|
+
export type { OutputFormat, RenderContext, RenderSurface };
|
|
@@ -4,7 +4,9 @@ import type { CustomProp, ExternalRel } from './ooxml-metadata.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Pulls the side-channel content out of a .docx zip — every text-bearing
|
|
6
6
|
* surface mammoth drops on the floor: core / app / custom doc properties,
|
|
7
|
-
* people registry, external hyperlinks, comments, tracked changes (
|
|
7
|
+
* people registry, external hyperlinks, comments, tracked changes (replacements,
|
|
8
|
+
* the insertions and deletions that pair with nothing, moves, and run /
|
|
9
|
+
* paragraph formatting changes),
|
|
8
10
|
* hidden text (w:vanish), text-box / shape text (w:txbxContent), header/footer
|
|
9
11
|
* body prose, field instructions (MERGEFIELD / HYPERLINK / DOCVARIABLE), bookmarks.
|
|
10
12
|
*
|
|
@@ -36,6 +38,28 @@ type TrackedChange = {
|
|
|
36
38
|
readonly date: string;
|
|
37
39
|
readonly text: string;
|
|
38
40
|
};
|
|
41
|
+
type Replacement = {
|
|
42
|
+
readonly deletionId: string;
|
|
43
|
+
readonly insertionId: string;
|
|
44
|
+
readonly author: string;
|
|
45
|
+
readonly date: string;
|
|
46
|
+
readonly before: string;
|
|
47
|
+
readonly after: string;
|
|
48
|
+
};
|
|
49
|
+
type Move = {
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly author: string;
|
|
52
|
+
readonly date: string;
|
|
53
|
+
readonly text: string;
|
|
54
|
+
readonly halves: 'both' | 'from-only' | 'to-only';
|
|
55
|
+
};
|
|
56
|
+
type FormatChange = {
|
|
57
|
+
readonly scope: 'run' | 'paragraph';
|
|
58
|
+
readonly author: string;
|
|
59
|
+
readonly date: string;
|
|
60
|
+
readonly text: string;
|
|
61
|
+
readonly properties: ReadonlyArray<string>;
|
|
62
|
+
};
|
|
39
63
|
type Field = {
|
|
40
64
|
readonly source: string;
|
|
41
65
|
readonly instruction: string;
|
|
@@ -57,6 +81,12 @@ type DocxMetadata = {
|
|
|
57
81
|
readonly comments: ReadonlyArray<Comment>;
|
|
58
82
|
readonly insertions: ReadonlyArray<TrackedChange>;
|
|
59
83
|
readonly deletions: ReadonlyArray<TrackedChange>;
|
|
84
|
+
/** A deletion and the insertion beside it, reported as the one edit they are. */
|
|
85
|
+
readonly replacements: ReadonlyArray<Replacement>;
|
|
86
|
+
/** Text moved elsewhere, joined by the range name that brackets both halves. */
|
|
87
|
+
readonly moves: ReadonlyArray<Move>;
|
|
88
|
+
/** Run or paragraph properties changed under revision marking. */
|
|
89
|
+
readonly formatChanges: ReadonlyArray<FormatChange>;
|
|
60
90
|
readonly hiddenText: ReadonlyArray<string>;
|
|
61
91
|
readonly textBoxes: ReadonlyArray<string>;
|
|
62
92
|
readonly headersFooters: ReadonlyArray<HeaderFooter>;
|
|
@@ -66,4 +96,4 @@ type DocxMetadata = {
|
|
|
66
96
|
};
|
|
67
97
|
declare const extractDocxMetadata: (bytes: Uint8Array) => Promise<Result<DocxMetadata, GraphError>>;
|
|
68
98
|
export { extractDocxMetadata };
|
|
69
|
-
export type { Bookmark, Comment, CustomProp, DocxMetadata, ExternalRel, Field, HeaderFooter, Person, TrackedChange };
|
|
99
|
+
export type { Bookmark, Comment, CustomProp, DocxMetadata, ExternalRel, Field, FormatChange, HeaderFooter, Move, Person, Replacement, TrackedChange };
|
|
@@ -13,15 +13,25 @@ declare const schema: z.ZodObject<{}, z.core.$strict>;
|
|
|
13
13
|
* loop with no exit: the command that needs elevated says "run login", login
|
|
14
14
|
* says "authenticated", the command fails identically, forever.
|
|
15
15
|
*
|
|
16
|
-
* When elevated is confirmed missing we
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* When elevated is confirmed missing we try `getElevatedAccessToken` FIRST. It
|
|
17
|
+
* drives a silent SSO against the persistent browser profile and leaves cookies
|
|
18
|
+
* untouched, so the tenant's 90-day ESTSAUTHPERSISTENT session survives and the
|
|
19
|
+
* user sees a window flash rather than a sign-in page. The `{ force: true }`
|
|
20
|
+
* dance calls `context.clearCookies()` to make the OAuth grant re-fire for the
|
|
21
|
+
* BASIC token, and that wipe deletes ESTSAUTHPERSISTENT — which is why routing
|
|
22
|
+
* every elevated expiry through it produced a credential prompt each time, and
|
|
23
|
+
* then destroyed the very session the prompt had just established.
|
|
21
24
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
+
* The silent route is not always available (expired profile cookies, a failed
|
|
26
|
+
* launch, a blocked navigation), so any failure falls back to the forced dance:
|
|
27
|
+
* it can prompt, but it polls for five minutes and a human can finish it.
|
|
28
|
+
*
|
|
29
|
+
* Supersedes the 2026-07-16 decision to escalate straight to `{ force: true }`.
|
|
30
|
+
* That entry feared a hand-rolled re-capture would no-op against the browser
|
|
31
|
+
* adapter's `freshCachedToken` probe; the probe lives in `acquireBothTokens`,
|
|
32
|
+
* not in `acquireElevatedToken`, and elevated freshness is decided upstream by
|
|
33
|
+
* `freshElevatedToken`. Verified live 2026-08-30: silent capture in 17s with no
|
|
34
|
+
* prompt, and the same call failing against a profile a forced login had wiped.
|
|
25
35
|
*/
|
|
26
36
|
declare const execute: (auth: AuthManager, options?: {
|
|
27
37
|
force?: boolean;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
type XmlObject = Record<string, unknown>;
|
|
18
18
|
declare const parseXml: (xml: string | undefined) => unknown;
|
|
19
19
|
declare const findAll: (root: unknown, tagName: string) => ReadonlyArray<XmlObject>;
|
|
20
|
+
declare const findAllLocal: (root: unknown, localName: string) => ReadonlyArray<XmlObject>;
|
|
20
21
|
declare const textOf: (node: unknown) => string;
|
|
21
22
|
declare const attrOf: (node: XmlObject, name: string) => string;
|
|
22
23
|
/**
|
|
@@ -26,11 +27,35 @@ declare const attrOf: (node: XmlObject, name: string) => string;
|
|
|
26
27
|
* flattens every match into a single string for "the visible text of this run".
|
|
27
28
|
*/
|
|
28
29
|
declare const findAllTexts: (root: unknown, tagName: string) => ReadonlyArray<string>;
|
|
30
|
+
declare const collectText: (node: unknown, tagName: string) => string;
|
|
31
|
+
declare const collectTextLocal: (node: unknown, localName: string) => string;
|
|
29
32
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* down to its visible text by gathering every `<w:t>` (or `<w:delText>`) descendant.
|
|
33
|
+
* One element in the `preserveOrder` shape: its tag name, and the node itself
|
|
34
|
+
* (whose `[tag]` holds the ordered children and whose `:@` holds attributes).
|
|
33
35
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
type OrderedNode = {
|
|
37
|
+
readonly tag: string;
|
|
38
|
+
readonly node: XmlObject;
|
|
39
|
+
};
|
|
40
|
+
declare const parseXmlOrdered: (xml: string | undefined) => unknown;
|
|
41
|
+
/**
|
|
42
|
+
* Every sibling list in the tree, each in document order, elements only.
|
|
43
|
+
*
|
|
44
|
+
* Sibling lists rather than one flat stream: adjacency only means anything
|
|
45
|
+
* WITHIN a parent. A deletion ending one paragraph and an insertion opening the
|
|
46
|
+
* next are consecutive in a flat walk and unrelated in the document.
|
|
47
|
+
*/
|
|
48
|
+
declare const orderedSiblingGroups: (root: unknown) => ReadonlyArray<ReadonlyArray<OrderedNode>>;
|
|
49
|
+
/**
|
|
50
|
+
* Every element in true document order, pre-order (an element, then what it
|
|
51
|
+
* contains). Unlike `orderedSiblingGroups`, which reports a whole sibling level
|
|
52
|
+
* before descending, this is the order the start tags appear in the file, which
|
|
53
|
+
* is what reading flat range markers requires: `<w:moveFromRangeStart>` opens a
|
|
54
|
+
* span that a later element sits inside without being its child.
|
|
55
|
+
*/
|
|
56
|
+
declare const orderedElements: (root: unknown) => ReadonlyArray<OrderedNode>;
|
|
57
|
+
declare const orderedAttrOf: (node: XmlObject, name: string) => string;
|
|
58
|
+
/** The `collectText` of the ordered shape: flatten every `tagName` descendant to one string. */
|
|
59
|
+
declare const collectOrderedText: (node: XmlObject, tagName: string) => string;
|
|
60
|
+
export { attrOf, collectOrderedText, collectText, collectTextLocal, findAll, findAllLocal, findAllTexts, orderedAttrOf, orderedElements, orderedSiblingGroups, parseXml, parseXmlOrdered, textOf, };
|
|
61
|
+
export type { OrderedNode, XmlObject };
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type { OoxmlZip } from '../../infra/ooxml-zip-adapter.js';
|
|
2
2
|
/**
|
|
3
3
|
* PowerPoint comments come in two formats: legacy (`ppt/commentAuthors.xml`
|
|
4
|
-
* authors by integer id + `ppt/comments/comment*.xml` `<
|
|
5
|
-
*
|
|
6
|
-
* `ppt/comments/*.xml` `<
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* authors by integer id + `ppt/comments/comment*.xml` `<cm authorId dt>` with a
|
|
5
|
+
* `<text>` body) and modern (`ppt/authors.xml` authors by GUID +
|
|
6
|
+
* `ppt/comments/*.xml` `<cm authorId created>` with a DrawingML `<t>` body).
|
|
7
|
+
* Elements are matched by local name, so the prefixes a writer happens to bind
|
|
8
|
+
* (`p:` / `p188:` / `a:` from PowerPoint, anything else from a third-party tool)
|
|
9
|
+
* never decide whether a comment is found. Both generations share the local name
|
|
10
|
+
* `cm`, so one pass reads them all; authors are resolved by id in either scheme.
|
|
9
11
|
*/
|
|
10
12
|
type CommentAuthor = {
|
|
11
13
|
readonly id: string;
|
|
12
14
|
readonly name: string;
|
|
13
15
|
readonly initials: string;
|
|
16
|
+
readonly email: string;
|
|
14
17
|
};
|
|
15
18
|
type PptxComment = {
|
|
16
19
|
readonly author: string;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
declare const TIER_CAPABILITY: Readonly<Record<string, string>>;
|
|
2
|
+
declare const OPTIONAL_TIERS: readonly ["elevated", "chatsvcagg", "ic3"];
|
|
3
|
+
type OptionalTier = (typeof OPTIONAL_TIERS)[number];
|
|
4
|
+
export { OPTIONAL_TIERS, TIER_CAPABILITY };
|
|
5
|
+
export type { OptionalTier };
|
package/docs/COMMANDS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Command reference
|
|
2
2
|
|
|
3
|
-
All
|
|
3
|
+
All 186 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 six lifecycle commands (`login`, `logout`, `update`, `docs`, `help-json`, `mcp`) are listed separately in the **Authentication & lifecycle** section below — `help-json` counts them in its manifest total (192), so a 186-vs-192 gap is those six, 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
|
|
|
@@ -146,15 +146,17 @@ For everything else:
|
|
|
146
146
|
| `list-group-conversations` | List conversations in a unified (Microsoft 365) group inbox. Each conversation aggregates one or more threads. Only Microsoft 365 groups have a mailbox — security and distribution groups return `MailboxNotEnabledForRESTAPI`. Verify the group is unified before calling. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/conversations` |
|
|
147
147
|
| `list-group-threads` | List threads in a unified (Microsoft 365) group inbox. Threads are flatter than conversations — one per topic, useful when conversation-level grouping isn't needed. Only Microsoft 365 groups have a mailbox — security and distribution groups return `MailboxNotEnabledForRESTAPI`. | `--group-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /groups/{group-id}/threads` |
|
|
148
148
|
| `list-mail-attachments` | List the attachments (file, item, reference) on a single Outlook message. The CLI ships an opinionated default `--select=id,name,contentType,size,isInline` so an LLM that doesn't slim the response itself doesn't accidentally pull multi-MB `contentBytes` for every attachment (a single 1.5 MB image attachment would otherwise blow the context window). The `@odata.type` discriminator is always returned by Graph regardless of `$select` (and Graph rejects asking for it explicitly). To fetch the actual bytes, call `get-mail-attachment` for the one you need (or override `--select` if you really want the raw inline payload). | `--message-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages/{message-id}/attachments` |
|
|
149
|
-
| `list-mail-child-folders` | List the subfolders of a single Outlook mail folder (e.g. subfolders of Inbox). | `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/childFolders` |
|
|
149
|
+
| `list-mail-child-folders` | List the subfolders of a single Outlook mail folder (e.g. subfolders of Inbox). | `--mail-folder-id`, `--include-hidden-folders`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/childFolders` |
|
|
150
150
|
| `list-mail-folder-messages` | List the messages inside a specific Outlook mail folder (Inbox, custom folder, etc.). | `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/messages` |
|
|
151
151
|
| `list-mail-folder-messages-delta` | Track incremental changes (added / updated / deleted messages) within a single mail folder using Microsoft Graph delta tokens. The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed since. `--top` is translated into the `Prefer: odata.maxpagesize=N` header: as a `$top` query parameter Graph reads a satisfied count as "sync complete" and hands back a deltaLink after N items, silently abandoning the rest of the folder. `$skip` and `$orderby` are NOT exposed — Graph ignores the former on this endpoint and rejects the latter unless it merely restates the default `receivedDateTime desc`. | `--mail-folder-id`, `--top`, `--select`, `--filter`, `--expand` | `GET /me/mailFolders/{mail-folder-id}/messages/delta()` |
|
|
152
|
-
| `list-mail-folders` | List the top-level mail folders in the signed-in user’s Outlook mailbox (Inbox, Sent Items, etc.). | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders` |
|
|
152
|
+
| `list-mail-folders` | List the top-level mail folders in the signed-in user’s Outlook mailbox (Inbox, Sent Items, etc.). | `--include-hidden-folders`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/mailFolders` |
|
|
153
153
|
| `list-mail-folders-delta` | Track incremental changes to the mail-folder tree itself (folders added / renamed / deleted). The first call returns the current snapshot plus a `@odata.deltaLink`; subsequent calls with that link return only what has changed. Companion to `list-mail-folder-messages-delta` which tracks message changes inside one folder. Note: Graph explicitly rejects `$top`, `$filter`, `$orderby`, and `$search` on this delta endpoint (`ErrorInvalidUrlQuery: not supported with change tracking over the 'Folders' resource`), so the OData passthrough is intentionally NOT exposed here. | _(none)_ | `GET /me/mailFolders/delta()` |
|
|
154
154
|
| `list-mail-messages` | List the most recent messages from across the signed-in user's entire Outlook mailbox (every folder including Sent, Archive, Junk; default sort `receivedDateTime` desc). The CLI ships a slim default `--select=id,subject,from,toRecipients,ccRecipients,receivedDateTime,hasAttachments,isRead,importance,bodyPreview,conversationId` (`conversationId` groups messages into a thread and can be handed to `list-conversation-messages`) so a page of 25 messages stays ~30-60 KB instead of ~1 MB. Pass `--select id,subject,body` (or any other comma-separated field list) to override. Use `list-mail-folder-messages` to scope to a single folder such as Inbox. | `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /me/messages` |
|
|
155
155
|
| `list-mail-rules` | List the message rules on the Outlook Inbox. Microsoft Graph only supports message rules on the Inbox folder; passing any other folder ID (drafts, sentitems, archive, a custom folder) returns `MailFolderNotSupportedError` from Graph. `--mail-folder-id` defaults to `inbox` because that is the only value Graph accepts; the flag is kept (optional) for callers that want to pass a resolved Inbox ID explicitly. Note: Graph silently ignores every OData passthrough on this endpoint, so the CLI does NOT expose them — the full rule set is always returned. | `--mail-folder-id` | `GET /me/mailFolders/{mail-folder-id}/messageRules` |
|
|
156
156
|
| `list-outlook-categories` | List the signed-in user's Outlook color categories — the named tags that can be applied to mail, calendar items, and contacts. Each entry has `displayName` and a `color` from Outlook's preset palette. Note: Graph silently ignores every OData passthrough on this endpoint (`$top`, `$skip`, `$select`, `$filter`, `$orderby`, `$expand`), so the CLI does not expose any of those flags — the full collection is always returned. Slice client-side. | _(none)_ | `GET /me/outlook/masterCategories` |
|
|
157
|
+
| `list-shared-mailbox-child-folders` | List the subfolders of one mail folder in a shared or delegated mailbox. The `/me` sibling is `list-mail-child-folders`. Walk it from the folder IDs `list-shared-mailbox-folders` returns to reach nested custom folders. 403 if the signed-in user does not have shared access to that mailbox. | `--user-id`, `--mail-folder-id`, `--include-hidden-folders`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/mailFolders/{mail-folder-id}/childFolders` |
|
|
157
158
|
| `list-shared-mailbox-folder-messages` | List messages in a single folder of a shared / delegated mailbox. | `--user-id`, `--mail-folder-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/mailFolders/{mail-folder-id}/messages` |
|
|
159
|
+
| `list-shared-mailbox-folders` | List the top-level mail folders of a shared or delegated mailbox. The `/me` sibling is `list-mail-folders`. Use it to discover the folder IDs that `list-shared-mailbox-folder-messages` needs: without it only the well-known names (`inbox`, `sentitems`, `drafts`, …) are reachable, so custom folders are invisible. 403 if the signed-in user does not have shared access to that mailbox. | `--user-id`, `--include-hidden-folders`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/mailFolders` |
|
|
158
160
|
| `list-shared-mailbox-messages` | List messages from a shared or delegated mailbox the signed-in user has read access to. Same shape as `list-mail-messages` but scoped to a specific mailbox owner. 403 if the signed-in user does not have shared access to that mailbox. | `--user-id`, `--top`, `--skip`, `--select`, `--filter`, `--orderby`, `--expand` | `GET /users/{user-id}/messages` |
|
|
159
161
|
| `read-mail-attachment` | Read an Outlook mail attachment whatever it is — one command that auto-routes by file type, preferring the content-type when the filename extension is misleading (a real `.jpg` that is actually a spreadsheet still converts), so a caller never has to choose between the convert-mail-attachment-* siblings. A `.zip` fileAttachment is unpacked and every entry converted (mirrors `convert-mail-attachment-zip-to-markdown`, returning the `{ count, files }` envelope; legacy GBK/CP437 names decoded). Any other attachment — docx/xlsx/pptx/odt/ods/odp + macro/template variants → markdown, csv → table, pdf → text layer (with `pageCount`), legacy .xls/.doc extracted, an inner Outlook .msg rendered recursively (quoted chain stripped unless `--keep-quoted true`), plain text passed through, referenceAttachment resolved via `/shares`, and itemAttachment (embedded mail/event/contact) rendered — goes through the same dispatch as `convert-mail-attachment-to-markdown` (returning its `{ contentType, size, text }` envelope). Images, scanned/image-only PDFs, and legacy .ppt return an actionable 415 pointing at `convert-mail-attachment-to-pdf` + a vision model or `get-mail-attachment` for the raw bytes. Pass `--include-metadata true` to append Office side-channel metadata. Use the explicit `convert-mail-attachment-to-markdown` / `-to-pdf` / `-zip` siblings only when you need to force a specific output format. | `--message-id`, `--attachment-id`, `--include-metadata`, `--keep-quoted` | `GET /me/messages/{message-id}/attachments/{attachment-id}` |
|
|
160
162
|
| `resolve-mail-link` | Parse a Microsoft Outlook web mail link (the URL emitted by the "Copy link" / address-bar share of an email) into its `messageId`. Pure transformation — no Graph call. Pipe the result into `get-mail-message` to fetch the body, or `convert-mail-to-markdown` to render it. For Outlook calendar links use `resolve-calendar-link` instead — this command rejects them with a pointer. | `--url` | `GET {url}` |
|
|
@@ -186,7 +188,7 @@ For everything else:
|
|
|
186
188
|
| `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` |
|
|
187
189
|
| `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` |
|
|
188
190
|
| `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` |
|
|
189
|
-
| `get-user` | Look up a directory user. Pass an Azure AD id, UPN, or email as --user-id and get that user's FULL profile (displayName, mail, jobTitle, department, officeLocation, phones) via GET /users/{id} on the elevated M365 token
|
|
191
|
+
| `get-user` | Look up a directory user. Pass an Azure AD id, UPN, or email as --user-id and get that user's FULL profile (displayName, mail, jobTitle, department, officeLocation, phones) via GET /users/{id} on the basic token — no elevated login needed. On a tenant that restricts basic directory reads the id path falls back to the elevated M365 token; re-capture that with `ask-marcel-office login` (preflight tiers with `ask-marcel-office scopes-check`, no Graph call). An email resolves even when it is the user's `mail` rather than their sign-in UPN: guest / B2B users carry a `#EXT#` UPN whose local part is NOT their email address, so when the direct lookup 404s the command falls back to `GET /users?$filter=mail eq '<email>'` and returns the single match. Only THIS tenant's directory is queried: a person's home-tenant object id (e.g. a cross-tenant Teams `8:orgid:<home-id>` participant, whose id lives in their own tenant) and any email that is not their `mail`/UPN here are unresolvable by design — reach an external person via their LOCAL guest projection (by name, or by their real `mail`), never by their home id. Pass a NAME instead and it searches your relevant-people graph (GET /me/people) and returns candidate matches (id, displayName, mail, jobTitle, department) so you can pick the right person and re-query. Re-query by the candidate's `id` when it is a directory GUID; an EXTERNAL contact's candidate carries a base64-ish People-API id instead, which nothing can resolve — re-query those by the candidate's `mail` (the CLI rejects an opaque contact id with that remedy rather than returning empty matches). Name search covers colleagues in your people graph, not the whole tenant directory; use `microsoft-search-query` for a broader tenant-wide person search. Without `--select`, the profile ships a default projection of id, displayName, userPrincipalName, mail, jobTitle, department, officeLocation, businessPhones, mobilePhone. | `--user-id`, `--select`, `--expand` | `GET /users/{user-id} (id / UPN; an email that misses falls back to /users?$filter=mail eq) OR /me/people?$search="{user-id}" (a bare name)` |
|
|
190
192
|
| `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` |
|
|
191
193
|
| `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` |
|
|
192
194
|
| `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` |
|
|
@@ -261,7 +263,7 @@ For everything else:
|
|
|
261
263
|
| `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-to-markdown` 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` |
|
|
262
264
|
| `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` |
|
|
263
265
|
| `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` |
|
|
264
|
-
| `next-page` | Fetch the next page of a paginated Graph response. Pass the cursor the previous command emitted — in text mode the `---` footer prints the whole ready-to-run command (`next: ask-marcel-office next-page --url '<url>'`), so copy the line as-is (the URL is single-quoted because it contains `$`); in JSON mode use 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}` |
|
|
265
|
-
| `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. Every tier block also lists that token's OWN granted scopes (decoded from its `scp` claim, distinct per token) and its `refresh` route (`automatic` = self-heals from the shared refresh token; `interactive` = the elevated tier, which carries no refresh token of its own and is re-captured by a browser login); the `hint` field says how to refresh them. | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
|
|
266
|
+
| `next-page` | Fetch the next page of a paginated Graph response. Pass the cursor the previous command emitted — in text mode the `---` footer prints the whole ready-to-run command (`next: ask-marcel-office next-page --url '<url>'`), so copy the line as-is (the URL is single-quoted because it contains `$`); in JSON mode use 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. When the cursor came from a partner-tenant (guest) drive listing, pass the same `--tenant-id` you used on the originating command, since the cursor carries no tenant and without it page 2 fails with `invalidAudienceUri`. | `--url`, `--tenant-id` | `GET {url}` |
|
|
267
|
+
| `scopes-check` | Decode the cached Teams web client access token and return its scopes, audience, and expiry without making a Graph call. It never opens a browser: an expired or missing session is reported as such (negative `expiresInSeconds`, or a not-signed-in error), and `login` is the command that refreshes it. 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. Every tier block also lists that token's OWN granted scopes (decoded from its `scp` claim, distinct per token) and its `refresh` route (`automatic` = self-heals from the shared refresh token; `interactive` = the elevated tier, which carries no refresh token of its own and is re-captured by a browser login); the `hint` field says how to refresh them. | _(none)_ | `GET (meta) cached-token introspection — no Graph endpoint` |
|
|
266
268
|
|
|
267
269
|
<!-- AUTO-GENERATED-COMMANDS:END -->
|
package/docs/USAGE.md
CHANGED
|
@@ -18,7 +18,7 @@ bun add -g ask-marcel-office-cli
|
|
|
18
18
|
ask-marcel-office login
|
|
19
19
|
|
|
20
20
|
# the rest is discoverable
|
|
21
|
-
ask-marcel-office --help # ~
|
|
21
|
+
ask-marcel-office --help # ~34 KB, one-sentence summaries
|
|
22
22
|
ask-marcel-office help-json --terse --category mail # ~6 KB JSON for one category
|
|
23
23
|
ask-marcel-office docs list-mail-messages # full per-command Markdown
|
|
24
24
|
```
|
|
@@ -306,7 +306,7 @@ Environment variables read at composition time:
|
|
|
306
306
|
## Quality gates (atelier four-check loop)
|
|
307
307
|
|
|
308
308
|
```bash
|
|
309
|
-
bun test # full suite (
|
|
309
|
+
bun test # full suite (4,700+ tests)
|
|
310
310
|
bun run lint # ESLint (0 warnings, 0 errors)
|
|
311
311
|
bun run typecheck # tsc --noEmit
|
|
312
312
|
bun run coverage # per-tier gates (100% on every tier: domain, use-cases, infra, composition, presenter)
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ask-marcel-office-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.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>",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/ask-marcel/ask-marcel-office-cli.git"
|
|
10
10
|
},
|
|
11
11
|
"bugs": {
|
|
12
|
-
"url": "https://github.com/
|
|
12
|
+
"url": "https://github.com/ask-marcel/ask-marcel-office-cli/issues"
|
|
13
13
|
},
|
|
14
|
-
"homepage": "https://github.com/
|
|
14
|
+
"homepage": "https://github.com/ask-marcel/ask-marcel-office-cli#readme",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "./dist/index.js",
|
|
17
17
|
"module": "./dist/index.js",
|
|
@@ -44,19 +44,35 @@
|
|
|
44
44
|
},
|
|
45
45
|
"keywords": [
|
|
46
46
|
"microsoft-graph",
|
|
47
|
+
"microsoft-365",
|
|
47
48
|
"office-365",
|
|
49
|
+
"m365",
|
|
48
50
|
"cli",
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
51
|
+
"mcp",
|
|
52
|
+
"mcp-server",
|
|
53
|
+
"llm",
|
|
54
|
+
"ai-agent",
|
|
55
|
+
"ai-agents",
|
|
56
|
+
"claude",
|
|
57
|
+
"claude-code",
|
|
58
|
+
"cursor",
|
|
59
|
+
"markdown",
|
|
52
60
|
"outlook",
|
|
53
61
|
"teams",
|
|
54
62
|
"onedrive",
|
|
55
|
-
"sharepoint"
|
|
63
|
+
"sharepoint",
|
|
64
|
+
"calendar",
|
|
65
|
+
"excel",
|
|
66
|
+
"onenote",
|
|
67
|
+
"planner",
|
|
68
|
+
"graph-api",
|
|
69
|
+
"typescript",
|
|
70
|
+
"bun"
|
|
56
71
|
],
|
|
57
72
|
"scripts": {
|
|
58
73
|
"start": "bun run src/main.ts",
|
|
59
74
|
"lint": "eslint --cache --max-warnings=0",
|
|
75
|
+
"lint:staged": "bash scripts/lint-staged.sh",
|
|
60
76
|
"lint:strict": "LINT_STRICT=1 eslint --max-warnings=0",
|
|
61
77
|
"typecheck": "bun --bun x tsc --noEmit",
|
|
62
78
|
"coverage": "bun run scripts/check-coverage.ts",
|
|
@@ -99,7 +115,7 @@
|
|
|
99
115
|
"update-notifier": "^7.3.1",
|
|
100
116
|
"winston": "^3.19.0",
|
|
101
117
|
"word-extractor": "^1.0.4",
|
|
102
|
-
"xlsx": "
|
|
118
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
|
103
119
|
"zod": "^4.3.6"
|
|
104
120
|
}
|
|
105
121
|
}
|