ask-marcel-office-cli 2.1.0 → 2.2.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 (59) hide show
  1. package/CHANGELOG.md +203 -0
  2. package/README.md +178 -119
  3. package/dist/cli.js +44806 -27142
  4. package/dist/commands.json +371 -52
  5. package/dist/composition/build-deps.d.ts +8 -0
  6. package/dist/composition/mcp.d.ts +20 -0
  7. package/dist/composition/run-registry-command.d.ts +34 -0
  8. package/dist/domain/tenant-id.d.ts +9 -0
  9. package/dist/domain/utilities/spo-tenant.d.ts +20 -0
  10. package/dist/index.js +2735 -1674
  11. package/dist/infra/auth.d.ts +36 -14
  12. package/dist/infra/browser-auth.d.ts +7 -2
  13. package/dist/infra/graph-client.d.ts +52 -16
  14. package/dist/presenter/graph-cursor.d.ts +2 -0
  15. package/dist/presenter/output.d.ts +1 -1
  16. package/dist/presenter/render-to-string.d.ts +15 -0
  17. package/dist/use-cases/commands/build-command.d.ts +19 -2
  18. package/dist/use-cases/commands/command-types.d.ts +16 -11
  19. package/dist/use-cases/commands/convert-calendar-event-attachment-to-markdown.d.ts +4 -0
  20. package/dist/use-cases/commands/{convert-drive-item-zip.d.ts → convert-drive-item-zip-to-markdown.d.ts} +5 -0
  21. package/dist/use-cases/commands/{convert-local-file.d.ts → convert-local-file-to-markdown.d.ts} +4 -0
  22. package/dist/use-cases/commands/convert-mail-attachment-to-markdown.d.ts +10 -2
  23. package/dist/use-cases/commands/{convert-mail-attachment-zip.d.ts → convert-mail-attachment-zip-to-markdown.d.ts} +5 -1
  24. package/dist/use-cases/commands/convert-mail-to-markdown.d.ts +35 -1
  25. package/dist/use-cases/commands/create-forward-draft.d.ts +4 -0
  26. package/dist/use-cases/commands/create-reply-draft.d.ts +8 -0
  27. package/dist/use-cases/commands/download-drive-item-as-markdown.d.ts +7 -2
  28. package/dist/use-cases/commands/download-drive-item-as-pdf.d.ts +1 -0
  29. package/dist/use-cases/commands/download-drive-item-content.d.ts +1 -0
  30. package/dist/use-cases/commands/draft-comment-splicer.d.ts +29 -0
  31. package/dist/use-cases/commands/draft-dedup.d.ts +18 -0
  32. package/dist/use-cases/commands/draft-response.d.ts +4 -0
  33. package/dist/use-cases/commands/extract-drive-item-images.d.ts +1 -0
  34. package/dist/use-cases/commands/extract-local-file-images.d.ts +1 -1
  35. package/dist/use-cases/commands/fetch-raw-bytes.d.ts +13 -0
  36. package/dist/use-cases/commands/find-mail-drafts.d.ts +9 -0
  37. package/dist/use-cases/commands/get-mail-signature.d.ts +8 -0
  38. package/dist/use-cases/commands/get-user.d.ts +10 -0
  39. package/dist/use-cases/commands/inline-image-embedder.d.ts +2 -1
  40. package/dist/use-cases/commands/login-status.d.ts +9 -27
  41. package/dist/use-cases/commands/login.d.ts +21 -0
  42. package/dist/use-cases/commands/mail-message-select.d.ts +1 -0
  43. package/dist/use-cases/commands/mail-quote-stripper.d.ts +16 -15
  44. package/dist/use-cases/commands/markdown-dispatch.d.ts +2 -1
  45. package/dist/use-cases/commands/msg-to-markdown.d.ts +10 -1
  46. package/dist/use-cases/commands/odata-query.d.ts +15 -1
  47. package/dist/use-cases/commands/office-to-markdown.d.ts +1 -0
  48. package/dist/use-cases/commands/read-mail-attachment.d.ts +4 -0
  49. package/dist/use-cases/commands/resolve-command.d.ts +25 -0
  50. package/dist/use-cases/commands/search-all-files.d.ts +8 -0
  51. package/dist/use-cases/commands/search-escape.d.ts +23 -0
  52. package/dist/use-cases/commands/signature-extractor.d.ts +2 -0
  53. package/dist/use-cases/commands/tenant-option.d.ts +44 -0
  54. package/dist/use-cases/commands/update-mail-draft.d.ts +1 -0
  55. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +10 -5
  56. package/dist/use-cases/ports/filesystem.d.ts +1 -1
  57. package/docs/COMMANDS.md +34 -29
  58. package/docs/USAGE.md +65 -7
  59. package/package.json +2 -1
@@ -1,5 +1,6 @@
1
1
  import type { AccessToken } from '../domain/access-token.js';
2
2
  import type { Result } from '../domain/result.js';
3
+ import type { TenantId } from '../domain/tenant-id.js';
3
4
  import type { FileSystem } from '../use-cases/ports/filesystem.js';
4
5
  import type { Logger } from '../use-cases/ports/logger.js';
5
6
  import type { BrowserAuth, ElevatedFailureReason } from './browser-auth.js';
@@ -24,6 +25,11 @@ type ElevatedOutcome = {
24
25
  captured: false;
25
26
  reason: ElevatedFailureReason | 'unknown_error';
26
27
  };
28
+ type CachedTierInfo = {
29
+ readonly available: boolean;
30
+ readonly expiresInSeconds: number | undefined;
31
+ readonly scopes: ReadonlyArray<string>;
32
+ };
27
33
  type AuthManager = {
28
34
  getAccessToken: (options?: {
29
35
  force?: boolean;
@@ -34,6 +40,14 @@ type AuthManager = {
34
40
  * via headless Playwright. Used by the 3 historical-version commands.
35
41
  */
36
42
  getElevatedAccessToken: () => Promise<Result<AccessToken, AuthError>>;
43
+ /**
44
+ * Returns a Graph token issued by a PARTNER tenant's authority, for a user
45
+ * who is a guest there. Without it, every call against that tenant's
46
+ * SharePoint dies at `401 invalidAudienceUri` — home-tenant Graph cannot mint
47
+ * an SPO token for a foreign tenant. Cache -> headless redemption of the
48
+ * shared refresh token; never a browser.
49
+ */
50
+ getGuestAccessToken: (tenantId: TenantId) => Promise<Result<AccessToken, AuthError>>;
37
51
  /**
38
52
  * Returns a chatsvcagg-audience token (same Teams web client identity
39
53
  * as `getAccessToken`, but issued for the chatsvcagg resource). Falls
@@ -81,26 +95,32 @@ type AuthManager = {
81
95
  * Optional: only the real manager implements it; a minimal fake omits it
82
96
  * and callers treat that as unavailable. Never captures or refreshes.
83
97
  */
84
- getCachedElevatedInfo?: () => Promise<{
85
- available: boolean;
86
- expiresInSeconds: number | undefined;
87
- }>;
98
+ getCachedElevatedInfo?: () => Promise<CachedTierInfo>;
88
99
  /**
89
100
  * Same decode-only preflight as `getCachedElevatedInfo`, for the chatsvcagg /
90
101
  * ic3 Teams-chat substrate tokens. `login`'s four-token status and
91
102
  * `scopes-check` read these; a minimal fake omits them and callers treat that
92
103
  * as unavailable.
93
104
  */
94
- getCachedChatsvcaggInfo?: () => Promise<{
95
- available: boolean;
96
- expiresInSeconds: number | undefined;
97
- }>;
98
- getCachedIc3Info?: () => Promise<{
99
- available: boolean;
100
- expiresInSeconds: number | undefined;
101
- }>;
105
+ getCachedChatsvcaggInfo?: () => Promise<CachedTierInfo>;
106
+ getCachedIc3Info?: () => Promise<CachedTierInfo>;
107
+ };
108
+ /**
109
+ * Command names quoted in the secondary-token error messages, per token kind.
110
+ * The composition root derives these from the command registry
111
+ * (`needsElevatedToken` / `needsSubstrateToken` flags) and injects them; the
112
+ * defaults below are the corrected registry sets at the time of writing, so
113
+ * direct `createAuthManagerFromApi` callers still get accurate messages. The
114
+ * registry sets are pinned in meta.test.ts and the wiring in
115
+ * build-deps.test.ts, so drift surfaces there, not in a stale user message
116
+ * (the old hardcoded elevated list omitted `get-user`).
117
+ */
118
+ type SecondaryTokenCommands = {
119
+ readonly elevated: ReadonlyArray<string>;
120
+ readonly chatsvcagg: ReadonlyArray<string>;
121
+ readonly ic3: ReadonlyArray<string>;
102
122
  };
103
- declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, recaptureSecondaryViaBrowser?: boolean) => AuthManager;
123
+ declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem, recaptureSecondaryViaBrowser?: boolean, secondaryTokenCommands?: SecondaryTokenCommands, acquireBasicViaBrowser?: boolean) => AuthManager;
104
124
  /**
105
125
  * Probe the token cache for a fresh access token. Handed to the browser
106
126
  * capture so its poll loop can short-circuit the multi-minute dance
@@ -116,6 +136,8 @@ declare const createAuthManager: (deps: {
116
136
  fs?: FileSystem;
117
137
  browserProfileDir?: string;
118
138
  recaptureSecondaryViaBrowser?: boolean;
139
+ secondaryTokenCommands?: SecondaryTokenCommands;
140
+ acquireBasicViaBrowser?: boolean;
119
141
  }) => AuthManager;
120
142
  export { createAuthManager, createAuthManagerFromApi, createFreshCachedTokenProbe, stderrProgress };
121
- export type { AuthError, AuthManager, ElevatedOutcome };
143
+ export type { AuthError, AuthManager, CachedTierInfo, ElevatedOutcome, SecondaryTokenCommands };
@@ -97,8 +97,13 @@ type BrowserAuth = {
97
97
  * different Microsoft web app whose first-party app identity is on
98
98
  * Microsoft's allow-list for ODSP `logicalPermissions` (the scope our
99
99
  * Teams web client token lacks for historical-version stream
100
- * content). Reuses the persistent profile cookies — no second
101
- * sign-in. Headless by default.
100
+ * content). Launches a HEADED browser (`launchContext(false)`) and
101
+ * relies, best-effort, on the persistent profile's cookies to complete
102
+ * sign-in silently. This is NOT guaranteed: on tenants that do not
103
+ * persist SSO cookies (or once they lapse) the navigation lands on a
104
+ * sign-in wall and the capture times out (verified headed AND headless
105
+ * 2026-07-19). That is why it stays headed and why the caller treats a
106
+ * miss as non-fatal and points the user at an interactive `login`.
102
107
  *
103
108
  * Returns a discriminated union so the caller can distinguish the three
104
109
  * failure modes (browser-launch hang, navigation failure, silent-SSO
@@ -1,5 +1,6 @@
1
1
  import type { Result } from '../domain/result.js';
2
2
  import type { AuthManager } from '../infra/auth.js';
3
+ import type { TenantId } from '../domain/tenant-id.js';
3
4
  type GraphError = {
4
5
  type: 'api_error';
5
6
  status: number;
@@ -36,6 +37,34 @@ type GraphClient = {
36
37
  * elevated token).
37
38
  */
38
39
  getElevated: (path: string) => Promise<Result<unknown, GraphError>>;
40
+ /**
41
+ * JSON-GET signed with a PARTNER tenant's guest token instead of the home
42
+ * token. Required for any path that touches a tenant the user is only a guest
43
+ * in: home-tenant Graph cannot mint a SharePoint token for a foreign tenant,
44
+ * so those calls die at `401 invalidAudienceUri` no matter which home tier
45
+ * signs them.
46
+ *
47
+ * Get the `tenantId` from `resolve-drive-share-link` (it discovers it from the
48
+ * sharing URL) or from the caller's `--tenant-id`.
49
+ */
50
+ getGuest: (path: string, tenantId: TenantId) => Promise<Result<unknown, GraphError>>;
51
+ /**
52
+ * Binary twin of `getGuest`: follows the Graph 302 to the partner tenant's CDN
53
+ * and returns the bytes. The `fetchUrl` allow-list already admits any
54
+ * `*.sharepoint.com` / `*.svc.ms` host, so a partner tenant's download URL
55
+ * needs no special casing.
56
+ */
57
+ getBinaryGuest: (path: string, tenantId: TenantId) => Promise<Result<unknown, GraphError>>;
58
+ /**
59
+ * Resolves a SharePoint host to the Entra tenant that owns it, via the tenant's
60
+ * public OIDC discovery document. Unauthenticated: it asks "who owns this
61
+ * host?", not "what may I read?".
62
+ *
63
+ * This is what makes a bare sharing URL enough to cross tenants — the URL
64
+ * carries the host, the host names the tenant, and the tenant is the one thing
65
+ * `driveId` + `itemId` do not tell you.
66
+ */
67
+ discoverTenantId: (spoHost: string) => Promise<Result<TenantId, GraphError>>;
39
68
  /**
40
69
  * JSON-GET against the Teams chat substrate (post-2026-05:
41
70
  * `teams.microsoft.com/api/csa/<region>/api/v{N}/...` — see
@@ -102,6 +131,23 @@ type GraphClient = {
102
131
  */
103
132
  getCachedTokenInfo: () => Promise<Result<TokenInfo, GraphError>>;
104
133
  };
134
+ /**
135
+ * Decode-only status for one non-basic token tier: availability, remaining runway,
136
+ * the scopes granted to that token (decoded from its `scp`), and how it refreshes
137
+ * (`automatic` = rides the shared refresh token; `interactive` = elevated, needs a login).
138
+ */
139
+ type TokenTierInfo = {
140
+ readonly available: boolean;
141
+ readonly expiresInSeconds: number | undefined;
142
+ readonly scopes: ReadonlyArray<string>;
143
+ readonly refresh: 'automatic' | 'interactive';
144
+ /**
145
+ * Present ONLY when `available` is `false`: a one-line, jargon-free reason the
146
+ * tier is absent + how to restore it. Stops the empty `scopes: []` on a missing
147
+ * token from reading as a bug. Omitted entirely when the token is available.
148
+ */
149
+ readonly reason?: string;
150
+ };
105
151
  type TokenInfo = {
106
152
  readonly scopes: ReadonlyArray<string>;
107
153
  readonly audience: string | undefined;
@@ -122,24 +168,14 @@ type TokenInfo = {
122
168
  * preflight elevated access in a fresh process instead of turning every
123
169
  * version download into a `403`.
124
170
  */
125
- readonly elevated: {
126
- readonly available: boolean;
127
- readonly expiresInSeconds: number | undefined;
128
- };
171
+ readonly elevated: TokenTierInfo;
129
172
  /**
130
- * The two Teams-chat substrate tokens (chatsvcagg / ic3), same decode-only
131
- * `{ available, expiresInSeconds }` shape as `elevated`. `login` reports all four
132
- * tiers so a warm session can see every token's runway; both self-heal from the
133
- * shared refresh token, so they are informational rather than a preflight gate.
173
+ * The two Teams-chat substrate tokens (chatsvcagg / ic3), same `TokenTierInfo`
174
+ * shape as `elevated`. Both self-heal from the shared refresh token (refresh:
175
+ * automatic), so they are informational rather than a preflight gate.
134
176
  */
135
- readonly chatsvcagg: {
136
- readonly available: boolean;
137
- readonly expiresInSeconds: number | undefined;
138
- };
139
- readonly ic3: {
140
- readonly available: boolean;
141
- readonly expiresInSeconds: number | undefined;
142
- };
177
+ readonly chatsvcagg: TokenTierInfo;
178
+ readonly ic3: TokenTierInfo;
143
179
  };
144
180
  type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
145
181
  declare const createGraphClient: (auth: AuthManager, fetchFn?: FetchFn) => GraphClient;
@@ -0,0 +1,2 @@
1
+ declare const canonicalizeGraphCursor: (link: string) => string;
2
+ export { canonicalizeGraphCursor };
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from '../use-cases/ports/logger.js';
2
2
  import type { ErrorSource } from './error-hints.js';
3
- type OutputFormat = 'text' | 'json';
3
+ import type { OutputFormat } from './render-to-string.js';
4
4
  declare const render: (data: unknown, logger: Logger, format: OutputFormat) => void;
5
5
  declare const renderError: (message: string, format: OutputFormat, errorCode?: string, explicitSource?: ErrorSource, retryAfterSeconds?: number) => void;
6
6
  export { render, renderError };
@@ -0,0 +1,15 @@
1
+ import type { ErrorSource } from './error-hints.js';
2
+ type OutputFormat = 'text' | 'json';
3
+ /**
4
+ * Render a use-case success value to its final string, newline included.
5
+ * `output.ts` writes this to stdout; `mcp.ts` returns it as tool content.
6
+ */
7
+ declare const renderToString: (data: unknown, format: OutputFormat) => string;
8
+ /**
9
+ * Render an error to its final string, newline included. The `hint` / `source`
10
+ * lookup is shared with the CLI so an MCP consumer sees the same curated
11
+ * remedies (`error-hints.ts`) the terminal does.
12
+ */
13
+ declare const renderErrorToString: (message: string, format: OutputFormat, errorCode?: string, explicitSource?: ErrorSource, retryAfterSeconds?: number) => string;
14
+ export { renderErrorToString, renderToString };
15
+ export type { OutputFormat };
@@ -14,9 +14,26 @@ import { type ODataKey } from './odata-query.js';
14
14
  type SelectDefaults = {
15
15
  readonly defaultSelect?: string;
16
16
  };
17
+ /**
18
+ * `defaultTop`, when set and the user did NOT pass `--top`, injects a `$top`
19
+ * into the OData query string. Its purpose is NOT payload-slimming (that is
20
+ * `defaultSelect`) but cursor correctness: a handful of `$skip`-offset-paged
21
+ * Graph endpoints (`/me/people` is the canonical one) only emit a
22
+ * self-advancing `@odata.nextLink` when `$top` is present. Without it Graph
23
+ * echoes back the SAME `$skip` on every page (`?$skip=0` → `?$skip=0` → …), so
24
+ * following the cursor loops forever on page 1. Forcing a default `$top` makes
25
+ * Graph increment `$skip` by the page size. Only meaningful on list builders
26
+ * whose OData shape carries `top`. User-supplied `--top` always wins.
27
+ */
28
+ type ListDefaults = SelectDefaults & {
29
+ readonly defaultTop?: string;
30
+ };
31
+ declare const withDefaultSelect: <T extends {
32
+ readonly select?: string;
33
+ }>(data: T, defaultSelect: string | undefined) => T;
17
34
  declare const buildCommand: (pathFn: (params: Record<string, string>) => string, schema: z.ZodType) => Pick<Command, "schema" | "execute">;
18
35
  declare const buildElevatedCommand: (pathFn: (params: Record<string, string>) => string, schema: z.ZodType) => Pick<Command, "schema" | "execute">;
19
- declare const buildListCommand: <Shape extends z.ZodRawShape>(pathFn: (params: z.infer<z.ZodObject<Shape>>) => string, schema: z.ZodObject<Shape>, options?: SelectDefaults) => Pick<Command, "schema" | "execute">;
36
+ declare const buildListCommand: <Shape extends z.ZodRawShape>(pathFn: (params: z.infer<z.ZodObject<Shape>>) => string, schema: z.ZodObject<Shape>, options?: ListDefaults) => Pick<Command, "schema" | "execute">;
20
37
  declare const buildElevatedListCommand: <Shape extends z.ZodRawShape>(pathFn: (params: z.infer<z.ZodObject<Shape>>) => string, schema: z.ZodObject<Shape>, options?: SelectDefaults) => Pick<Command, "schema" | "execute">;
21
38
  /**
22
39
  * Single-resource GET that supports the OData `$select` and `$expand` query
@@ -67,4 +84,4 @@ declare const buildPickODataListCommand: <Shape extends z.ZodRawShape, K extends
67
84
  * `$orderby` / `$expand` with `BadRequest`, so the picker is the right tool.
68
85
  */
69
86
  declare const buildElevatedPickODataListCommand: <Shape extends z.ZodRawShape, K extends ODataKey>(pathFn: (params: z.infer<z.ZodObject<Shape>>) => string, schema: z.ZodObject<Shape>, keys: ReadonlyArray<K>, options?: SelectDefaults) => Pick<Command, "schema" | "execute">;
70
- export { buildCommand, buildElevatedCommand, buildElevatedListCommand, buildElevatedPickODataListCommand, buildElevatedSelectableCommand, buildFilterSelectListCommand, buildListCommand, buildNoSkipListCommand, buildPickODataListCommand, buildSelectableCommand, };
87
+ export { buildCommand, buildElevatedCommand, buildElevatedListCommand, buildElevatedPickODataListCommand, buildElevatedSelectableCommand, buildFilterSelectListCommand, buildListCommand, buildNoSkipListCommand, buildPickODataListCommand, buildSelectableCommand, withDefaultSelect, };
@@ -109,20 +109,25 @@ type CommandMeta = {
109
109
  readonly scopesRequired?: ReadonlyArray<string>;
110
110
  /**
111
111
  * `true` if the command needs the M365ChatClient elevated token (captured
112
- * at login from `m365.cloud.microsoft`, ODSP allow-list). Only 3 commands
113
- * today the historical-version downloads. An LLM should check this
114
- * field before invoking; if the elevated capture failed at login, these
115
- * commands will time out.
112
+ * at login from `m365.cloud.microsoft`, ODSP allow-list). The exact set is
113
+ * pinned in meta.test.ts, and the composition root derives the auth
114
+ * fail-fast message's command list from this flag a new elevated command
115
+ * MUST carry it or the remedy message will omit the command. An LLM should
116
+ * check this field before invoking; if the elevated capture failed at
117
+ * login, these commands fail fast with `secondary_token_unavailable`.
116
118
  */
117
119
  readonly needsElevatedToken?: true;
118
120
  /**
119
- * `true` if the command needs a Teams substrate token (chatsvcagg or ic3,
120
- * captured at login from `teams.microsoft.com`) the Teams chat-content
121
- * commands. Like `needsElevatedToken`, an LLM should check this before
122
- * invoking and warm up an interactive `login`; a headless or stale session
123
- * times out on these (the non-interactive silent-SSO limitation, ).
121
+ * Which Teams substrate token the command needs: `'chatsvcagg'`
122
+ * (teams.microsoft.com/api/csa) or `'ic3'` (chat history), both captured
123
+ * at login from `teams.microsoft.com`. Truthy = "needs a substrate token"
124
+ * (the historical boolean semantics); the service value additionally
125
+ * routes the command into the right auth fail-fast list, derived by the
126
+ * composition root. Like `needsElevatedToken`, an LLM should check this
127
+ * before invoking and warm up an interactive `login`; a headless or stale
128
+ * session times out on these (the non-interactive silent-SSO limitation).
124
129
  */
125
- readonly needsSubstrateToken?: true;
130
+ readonly needsSubstrateToken?: 'chatsvcagg' | 'ic3';
126
131
  /**
127
132
  * `true` if the command returns inlined bytes (`{contentType, size, base64}`
128
133
  * or `{contentType, size, text}`) and is therefore a valid target for the
@@ -167,7 +172,7 @@ type Command = {
167
172
  readonly meta: CommandMeta;
168
173
  /**
169
174
  * Present on the rare command whose input is the LOCAL filesystem instead of
170
- * Graph (`convert-local-file`). The CLI routes execution here, passing its
175
+ * Graph (`convert-local-file-to-markdown`). The CLI routes execution here, passing its
171
176
  * composition-selected FileSystem; `execute` stays as the registry-typed
172
177
  * fallback that redirects library consumers to this variant.
173
178
  */
@@ -9,6 +9,10 @@ declare const schema: z.ZodObject<{
9
9
  true: "true";
10
10
  false: "false";
11
11
  }>>;
12
+ keepQuoted: z.ZodOptional<z.ZodEnum<{
13
+ true: "true";
14
+ false: "false";
15
+ }>>;
12
16
  }, z.core.$strip>;
13
17
  declare const execute: (graph: GraphClient, params: Record<string, string>) => Promise<Result<unknown, GraphError>>;
14
18
  declare const meta: CommandMeta;
@@ -14,12 +14,17 @@ import type { CommandMeta } from './command-types.js';
14
14
  * rather than failing the whole archive.
15
15
  */
16
16
  declare const schema: z.ZodObject<{
17
+ tenantId: z.ZodOptional<z.ZodString>;
17
18
  driveId: z.ZodString;
18
19
  itemId: z.ZodString;
19
20
  includeMetadata: z.ZodOptional<z.ZodEnum<{
20
21
  true: "true";
21
22
  false: "false";
22
23
  }>>;
24
+ keepQuoted: z.ZodOptional<z.ZodEnum<{
25
+ true: "true";
26
+ false: "false";
27
+ }>>;
23
28
  }, z.core.$strip>;
24
29
  declare const execute: (graph: GraphClient, params: Record<string, string>) => Promise<Result<unknown, GraphError>>;
25
30
  declare const meta: CommandMeta;
@@ -29,6 +29,10 @@ declare const schema: z.ZodObject<{
29
29
  true: "true";
30
30
  false: "false";
31
31
  }>>;
32
+ keepQuoted: z.ZodOptional<z.ZodEnum<{
33
+ true: "true";
34
+ false: "false";
35
+ }>>;
32
36
  includeImages: z.ZodOptional<z.ZodEnum<{
33
37
  true: "true";
34
38
  false: "false";
@@ -9,9 +9,17 @@ declare const schema: z.ZodObject<{
9
9
  true: "true";
10
10
  false: "false";
11
11
  }>>;
12
+ keepQuoted: z.ZodOptional<z.ZodEnum<{
13
+ true: "true";
14
+ false: "false";
15
+ }>>;
12
16
  }, z.core.$strip>;
13
- declare const convertFetchedAttachment: (graph: GraphClient, a: Record<string, unknown>, includeMetadata: boolean) => Promise<Result<unknown, GraphError>> | Result<unknown, GraphError>;
14
- declare const convertAttachmentToMarkdown: (graph: GraphClient, attachmentPath: string, includeMetadata: boolean) => Promise<Result<unknown, GraphError>>;
17
+ type ConvertOptions = {
18
+ readonly includeMetadata: boolean;
19
+ readonly keepQuoted: boolean;
20
+ };
21
+ declare const convertFetchedAttachment: (graph: GraphClient, a: Record<string, unknown>, opts: ConvertOptions) => Promise<Result<unknown, GraphError>> | Result<unknown, GraphError>;
22
+ declare const convertAttachmentToMarkdown: (graph: GraphClient, attachmentPath: string, opts: ConvertOptions) => Promise<Result<unknown, GraphError>>;
15
23
  declare const execute: (graph: GraphClient, params: Record<string, string>) => Promise<Result<unknown, GraphError>>;
16
24
  declare const meta: CommandMeta;
17
25
  export { convertAttachmentToMarkdown, convertFetchedAttachment, execute, meta, schema };
@@ -4,7 +4,7 @@ import type { GraphClient, GraphError } from '../../infra/graph-client.js';
4
4
  import type { CommandMeta } from './command-types.js';
5
5
  /**
6
6
  * Unzips a `.zip` Outlook mail attachment and converts every contained file in one
7
- * call — the mail-side mirror of `convert-drive-item-zip`. Without it, reading a
7
+ * call — the mail-side mirror of `convert-drive-item-zip-to-markdown`. Without it, reading a
8
8
  * zipped vendor deck meant: `get-mail-attachment --output-path x.zip` → manual
9
9
  * `unzip` (with `-O GBK` for Chinese names) → convert each file. This collapses all
10
10
  * of that into one command: it pulls the fileAttachment bytes, unzips them (legacy
@@ -18,6 +18,10 @@ declare const schema: z.ZodObject<{
18
18
  true: "true";
19
19
  false: "false";
20
20
  }>>;
21
+ keepQuoted: z.ZodOptional<z.ZodEnum<{
22
+ true: "true";
23
+ false: "false";
24
+ }>>;
21
25
  }, z.core.$strip>;
22
26
  declare const execute: (graph: GraphClient, params: Record<string, string>) => Promise<Result<unknown, GraphError>>;
23
27
  declare const meta: CommandMeta;
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import type { Result } from '../../domain/result.js';
3
3
  import type { GraphClient, GraphError } from '../../infra/graph-client.js';
4
4
  import type { CommandMeta } from './command-types.js';
5
+ import { type InlineAttachment } from './inline-image-embedder.js';
5
6
  declare const schema: z.ZodObject<{
6
7
  messageId: z.ZodString;
7
8
  inlineImages: z.ZodOptional<z.ZodEnum<{
@@ -13,6 +14,39 @@ declare const schema: z.ZodObject<{
13
14
  false: "false";
14
15
  }>>;
15
16
  }, z.core.$strip>;
17
+ declare const ATTACHMENT_METADATA_SELECT = "$select=id,name,contentType,size,isInline,microsoft.graph.fileAttachment/contentId";
18
+ declare const attachmentMetaSchema: z.ZodObject<{
19
+ id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
20
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
21
+ contentType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
22
+ size: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
23
+ isInline: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
24
+ contentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
25
+ }, z.core.$strip>;
26
+ declare const attachmentsListSchema: z.ZodObject<{
27
+ value: z.ZodOptional<z.ZodArray<z.ZodObject<{
28
+ id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
29
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
+ contentType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
31
+ size: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
32
+ isInline: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
33
+ contentId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
34
+ }, z.core.$strip>>>;
35
+ }, z.core.$strip>;
36
+ type AttachmentMeta = z.infer<typeof attachmentMetaSchema>;
37
+ declare const formatBytes: (n: number) => string;
38
+ type InlineImageCandidate = AttachmentMeta & {
39
+ readonly contentType: string;
40
+ readonly contentId: string;
41
+ };
42
+ declare const isInlineImage: (a: AttachmentMeta) => a is InlineImageCandidate;
43
+ type EmbedFetchResult = {
44
+ readonly meta: InlineImageCandidate;
45
+ readonly inline?: InlineAttachment;
46
+ readonly oversize: boolean;
47
+ };
48
+ declare const fetchInlineImageBytes: (graph: GraphClient, messageId: string, meta: InlineImageCandidate) => Promise<EmbedFetchResult>;
16
49
  declare const execute: (graph: GraphClient, params: Record<string, string>) => Promise<Result<unknown, GraphError>>;
17
50
  declare const meta: CommandMeta;
18
- export { execute, meta, schema };
51
+ export { ATTACHMENT_METADATA_SELECT, attachmentsListSchema, execute, fetchInlineImageBytes, formatBytes, isInlineImage, meta, schema };
52
+ export type { AttachmentMeta, InlineImageCandidate };
@@ -6,6 +6,10 @@ declare const schema: z.ZodObject<{
6
6
  ccRecipients: z.ZodOptional<z.ZodString>;
7
7
  bodyContent: z.ZodString;
8
8
  subject: z.ZodOptional<z.ZodString>;
9
+ bodyContentType: z.ZodOptional<z.ZodEnum<{
10
+ Text: "Text";
11
+ HTML: "HTML";
12
+ }>>;
9
13
  }, z.core.$strip>;
10
14
  declare const execute: Command['execute'];
11
15
  declare const meta: CommandMeta;
@@ -4,6 +4,14 @@ declare const schema: z.ZodObject<{
4
4
  replyToMessageId: z.ZodString;
5
5
  bodyContent: z.ZodString;
6
6
  subject: z.ZodOptional<z.ZodString>;
7
+ replyAll: z.ZodOptional<z.ZodEnum<{
8
+ true: "true";
9
+ false: "false";
10
+ }>>;
11
+ bodyContentType: z.ZodOptional<z.ZodEnum<{
12
+ Text: "Text";
13
+ HTML: "HTML";
14
+ }>>;
7
15
  }, z.core.$strip>;
8
16
  declare const execute: Command['execute'];
9
17
  declare const meta: CommandMeta;
@@ -3,8 +3,6 @@ import type { Result } from '../../domain/result.js';
3
3
  import type { GraphClient, GraphError } from '../../infra/graph-client.js';
4
4
  import type { CommandMeta } from './command-types.js';
5
5
  declare const schema: z.ZodObject<{
6
- driveId: z.ZodString;
7
- itemId: z.ZodString;
8
6
  includeMetadata: z.ZodOptional<z.ZodEnum<{
9
7
  true: "true";
10
8
  false: "false";
@@ -13,7 +11,14 @@ declare const schema: z.ZodObject<{
13
11
  true: "true";
14
12
  false: "false";
15
13
  }>>;
14
+ keepQuoted: z.ZodOptional<z.ZodEnum<{
15
+ true: "true";
16
+ false: "false";
17
+ }>>;
16
18
  maxCells: z.ZodOptional<z.ZodString>;
19
+ tenantId: z.ZodOptional<z.ZodString>;
20
+ driveId: z.ZodString;
21
+ itemId: z.ZodString;
17
22
  }, z.core.$strip>;
18
23
  declare const execute: (graph: GraphClient, params: Record<string, string>) => Promise<Result<unknown, GraphError>>;
19
24
  declare const meta: CommandMeta;
@@ -3,6 +3,7 @@ import type { Result } from '../../domain/result.js';
3
3
  import type { GraphClient, GraphError } from '../../infra/graph-client.js';
4
4
  import type { CommandMeta } from './command-types.js';
5
5
  declare const schema: z.ZodObject<{
6
+ tenantId: z.ZodOptional<z.ZodString>;
6
7
  driveId: z.ZodString;
7
8
  itemId: z.ZodString;
8
9
  }, z.core.$strip>;
@@ -3,6 +3,7 @@ import type { Result } from '../../domain/result.js';
3
3
  import type { GraphClient, GraphError } from '../../infra/graph-client.js';
4
4
  import type { CommandMeta } from './command-types.js';
5
5
  declare const schema: z.ZodObject<{
6
+ tenantId: z.ZodOptional<z.ZodString>;
6
7
  driveId: z.ZodString;
7
8
  itemId: z.ZodString;
8
9
  }, z.core.$strip>;
@@ -0,0 +1,29 @@
1
+ type SpliceResult = {
2
+ readonly html: string;
3
+ readonly boundaryFound: boolean;
4
+ };
5
+ type PlainTextSpliceResult = {
6
+ readonly text: string;
7
+ readonly boundaryFound: boolean;
8
+ };
9
+ /**
10
+ * Renders an author's plain text as HTML: markup they typed shows as characters
11
+ * rather than taking effect, and their newlines survive as breaks. No wrapper
12
+ * element, so the caller decides the block context.
13
+ */
14
+ declare const escapeTextAsHtml: (text: string) => string;
15
+ /** Index just past the `<body>` open tag, or 0 for a fragment that has none. */
16
+ declare const findBodyInsertStart: (html: string) => number;
17
+ /**
18
+ * True when the author's own markup carries a quote boundary marker, e.g. they
19
+ * pasted a reply chain into it. Such a comment must be refused: the splice would
20
+ * keep the marker verbatim, and the NEXT revision would cut the draft AT it,
21
+ * silently dropping the real quoted history below.
22
+ */
23
+ declare const commentCarriesQuoteBoundary: (commentHtml: string) => boolean;
24
+ /** The refusal copy for `commentCarriesQuoteBoundary`, named for the flag that carried it. */
25
+ declare const boundaryMarkerRefusal: (flagName: string) => string;
26
+ declare const insertCommentAboveQuote: (html: string, commentHtml: string) => SpliceResult;
27
+ declare const replaceCommentAboveQuote: (html: string, commentHtml: string) => SpliceResult;
28
+ declare const replacePlainTextCommentAboveQuote: (text: string, comment: string) => PlainTextSpliceResult;
29
+ export { boundaryMarkerRefusal, commentCarriesQuoteBoundary, escapeTextAsHtml, findBodyInsertStart, insertCommentAboveQuote, replaceCommentAboveQuote, replacePlainTextCommentAboveQuote, };
@@ -0,0 +1,18 @@
1
+ declare const normalizeThreadSubject: (subject: string) => string;
2
+ type DraftRecipient = {
3
+ readonly emailAddress?: {
4
+ readonly address?: string;
5
+ };
6
+ };
7
+ type DraftForMatch = {
8
+ readonly subject?: string;
9
+ readonly toRecipients?: ReadonlyArray<DraftRecipient>;
10
+ readonly ccRecipients?: ReadonlyArray<DraftRecipient>;
11
+ };
12
+ type DraftMatchCriteria = {
13
+ readonly subject: string;
14
+ readonly recipients?: ReadonlyArray<string>;
15
+ };
16
+ declare const matchExistingDrafts: <T extends DraftForMatch>(drafts: ReadonlyArray<T>, criteria: DraftMatchCriteria) => ReadonlyArray<T>;
17
+ export { matchExistingDrafts, normalizeThreadSubject };
18
+ export type { DraftForMatch, DraftMatchCriteria };
@@ -0,0 +1,4 @@
1
+ import { type Result } from '../../domain/result.js';
2
+ import type { GraphError } from '../../infra/graph-client.js';
3
+ declare const slimDraftResult: (result: Result<unknown, GraphError>) => Result<unknown, GraphError>;
4
+ export { slimDraftResult };
@@ -3,6 +3,7 @@ import type { Result } from '../../domain/result.js';
3
3
  import type { GraphClient, GraphError } from '../../infra/graph-client.js';
4
4
  import type { CommandMeta } from './command-types.js';
5
5
  declare const schema: z.ZodObject<{
6
+ tenantId: z.ZodOptional<z.ZodString>;
6
7
  driveId: z.ZodString;
7
8
  itemId: z.ZodString;
8
9
  }, z.core.$strip>;
@@ -9,7 +9,7 @@ import type { CommandMeta } from './command-types.js';
9
9
  * (OOXML media parts / unpdf page walk). It completes two flows the drive
10
10
  * command cannot reach: a Graph-rendered PDF saved with the global
11
11
  * output-path flag (the legacy-.ppt route), and Office files unpacked from a
12
- * local archive. Like `convert-local-file`, it never touches Graph and is
12
+ * local archive. Like `convert-local-file-to-markdown`, it never touches Graph and is
13
13
  * executed via `executeLocal(fs, params)`.
14
14
  */
15
15
  declare const schema: z.ZodObject<{
@@ -1,4 +1,5 @@
1
1
  import type { Result } from '../../domain/result.js';
2
+ import type { TenantId } from '../../domain/tenant-id.js';
2
3
  import type { GraphClient, GraphError } from '../../infra/graph-client.js';
3
4
  /**
4
5
  * Helpers that consolidate the "Graph hands you a 302, follow the CDN
@@ -26,8 +27,20 @@ import type { GraphClient, GraphError } from '../../infra/graph-client.js';
26
27
  * redirects to a streamContent URL whose embedded tempauth is signed
27
28
  * by Teams web client identity and rejected by SharePoint with 403.
28
29
  */
30
+ /**
31
+ * `elevated` picks the ODSP-allow-listed home identity (M365ChatClient).
32
+ * `tenantId` picks a PARTNER tenant's guest identity — for a file in a tenant the
33
+ * user is only a guest in, which no home-tier token can read (Graph answers `401
34
+ * invalidAudienceUri`).
35
+ *
36
+ * They are mutually exclusive by nature: elevated is a home-tenant identity, so
37
+ * "elevated in a partner tenant" does not exist. `tenantId` wins if both arrive,
38
+ * because a partner-tenant file is unreadable on ANY home token — including the
39
+ * elevated one — so honouring `elevated` there would guarantee failure.
40
+ */
29
41
  export type FetchOptions = {
30
42
  readonly elevated?: boolean;
43
+ readonly tenantId?: TenantId;
31
44
  };
32
45
  export type InlineBinary = {
33
46
  readonly contentType: string;
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ import type { Command, CommandMeta } from './command-types.js';
3
+ declare const schema: z.ZodObject<{
4
+ subject: z.ZodString;
5
+ toRecipients: z.ZodOptional<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ declare const execute: Command['execute'];
8
+ declare const meta: CommandMeta;
9
+ export { execute, meta, schema };