ask-marcel-office-cli 1.0.0 → 1.5.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 (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +153 -384
  3. package/dist/cli.js +13222 -94112
  4. package/dist/commands.json +1500 -135
  5. package/dist/composition/build-deps.d.ts +13 -0
  6. package/dist/composition/cli.d.ts +8 -0
  7. package/dist/domain/iso-datetime.d.ts +30 -0
  8. package/dist/domain/utilities/archive-status.d.ts +18 -0
  9. package/dist/domain/utilities/site-url-classifier.d.ts +15 -0
  10. package/dist/index.js +10912 -92275
  11. package/dist/infra/auth.d.ts +56 -3
  12. package/dist/infra/browser-auth.d.ts +100 -10
  13. package/dist/infra/graph-client.d.ts +40 -0
  14. package/dist/infra/legacy-doc-extractor.d.ts +14 -0
  15. package/dist/infra/msg-reader-adapter.d.ts +83 -0
  16. package/dist/infra/network-error.d.ts +9 -0
  17. package/dist/infra/ooxml-media-extractor.d.ts +27 -0
  18. package/dist/infra/ooxml-zip-adapter.d.ts +28 -0
  19. package/dist/infra/pdf-image-extractor.d.ts +18 -0
  20. package/dist/infra/pdf-text-extractor.d.ts +41 -0
  21. package/dist/infra/png-encode.d.ts +15 -0
  22. package/dist/infra/sheetjs-adapter.d.ts +1 -1
  23. package/dist/infra/system-browser-auth.d.ts +35 -0
  24. package/dist/infra/system-browser-loader.d.ts +3 -0
  25. package/dist/infra/token-callback-server.d.ts +31 -0
  26. package/dist/infra/zip-reader.d.ts +32 -0
  27. package/dist/presenter/error-hints.d.ts +41 -0
  28. package/dist/presenter/output.d.ts +2 -1
  29. package/dist/use-cases/commands/build-command.d.ts +30 -8
  30. package/dist/use-cases/commands/command-types.d.ts +53 -1
  31. package/dist/use-cases/commands/convert-calendar-event-attachment-to-markdown.d.ts +15 -0
  32. package/dist/use-cases/commands/{download-drive-item-version-as-pdf.d.ts → convert-calendar-event-attachment-to-pdf.d.ts} +2 -3
  33. package/dist/use-cases/commands/convert-drive-item-zip.d.ts +26 -0
  34. package/dist/use-cases/commands/convert-local-file.d.ts +37 -0
  35. package/dist/use-cases/commands/convert-mail-attachment-to-markdown.d.ts +7 -1
  36. package/dist/use-cases/commands/convert-mail-attachment-to-pdf.d.ts +2 -1
  37. package/dist/use-cases/commands/convert-mail-attachment-zip.d.ts +24 -0
  38. package/dist/use-cases/commands/convert-mail-to-markdown.d.ts +8 -0
  39. package/dist/use-cases/commands/create-mail-draft.d.ts +22 -0
  40. package/dist/use-cases/commands/doc-to-markdown.d.ts +14 -0
  41. package/dist/use-cases/commands/docs-render.d.ts +7 -2
  42. package/dist/use-cases/commands/docs.d.ts +44 -1
  43. package/dist/use-cases/commands/docx-comment-anchors.d.ts +22 -0
  44. package/dist/use-cases/commands/docx-metadata-to-markdown.d.ts +3 -0
  45. package/dist/use-cases/commands/docx-metadata.d.ts +69 -0
  46. package/dist/use-cases/commands/docx-to-markdown.d.ts +8 -3
  47. package/dist/use-cases/commands/download-drive-item-as-markdown.d.ts +9 -0
  48. package/dist/use-cases/commands/download-drive-item-version.d.ts +21 -0
  49. package/dist/use-cases/commands/{download-drive-item-version-as-markdown.d.ts → extract-drive-item-images.d.ts} +0 -1
  50. package/dist/use-cases/commands/extract-local-file-images.d.ts +21 -0
  51. package/dist/use-cases/commands/{download-drive-item-version-content.d.ts → extract-mail-attachment-images.d.ts} +2 -3
  52. package/dist/use-cases/commands/extract-sharepoint-links-in-documents.d.ts +33 -0
  53. package/dist/use-cases/commands/extract-sharepoint-links-in-mail.d.ts +1 -8
  54. package/dist/use-cases/commands/fetch-raw-bytes.d.ts +2 -1
  55. package/dist/use-cases/commands/file-counts.d.ts +7 -0
  56. package/dist/use-cases/commands/filter-archived-sites.d.ts +17 -0
  57. package/dist/use-cases/commands/find-chats-with-user.d.ts +10 -0
  58. package/dist/use-cases/commands/first-sentence.d.ts +12 -0
  59. package/dist/use-cases/commands/get-chat.d.ts +1 -5
  60. package/dist/use-cases/commands/get-excel-chart-image.d.ts +20 -0
  61. package/dist/use-cases/commands/get-excel-used-range.d.ts +7 -2
  62. package/dist/use-cases/commands/get-onenote-page-as-markdown.d.ts +8 -0
  63. package/dist/use-cases/commands/get-teams-chat-message.d.ts +9 -0
  64. package/dist/use-cases/commands/get-todo-task.d.ts +9 -2
  65. package/dist/use-cases/commands/image-extraction.d.ts +10 -0
  66. package/dist/use-cases/commands/iso-datetime-schema.d.ts +21 -0
  67. package/dist/use-cases/commands/link-shape.d.ts +13 -0
  68. package/dist/use-cases/commands/list-accessible-drives.d.ts +14 -0
  69. package/dist/use-cases/commands/list-calendar-event-attachments.d.ts +14 -0
  70. package/dist/use-cases/commands/list-calendar-view-delta.d.ts +2 -2
  71. package/dist/use-cases/commands/list-teams-chat-history.d.ts +16 -0
  72. package/dist/use-cases/commands/list-teams-chat-messages.d.ts +8 -0
  73. package/dist/use-cases/commands/list-teams-chats-with-messages.d.ts +9 -0
  74. package/dist/use-cases/commands/mail-quote-stripper.d.ts +25 -0
  75. package/dist/use-cases/commands/markdown-dispatch.d.ts +29 -0
  76. package/dist/use-cases/commands/markdown-pipeline.d.ts +6 -2
  77. package/dist/use-cases/commands/media-files.d.ts +20 -0
  78. package/dist/use-cases/commands/msg-to-markdown.d.ts +28 -0
  79. package/dist/use-cases/commands/odf-content-to-markdown.d.ts +5 -0
  80. package/dist/use-cases/commands/odf-metadata-to-markdown.d.ts +3 -0
  81. package/dist/use-cases/commands/odf-metadata.d.ts +26 -0
  82. package/dist/use-cases/commands/odf-to-markdown.d.ts +17 -0
  83. package/dist/use-cases/commands/office-extensions.d.ts +13 -0
  84. package/dist/use-cases/commands/office-to-markdown.d.ts +6 -2
  85. package/dist/use-cases/commands/onenote-metadata.d.ts +21 -0
  86. package/dist/use-cases/commands/onenote-resource-embedder.d.ts +3 -0
  87. package/dist/use-cases/commands/ooxml-metadata-to-markdown.d.ts +16 -0
  88. package/dist/use-cases/commands/ooxml-metadata.d.ts +23 -0
  89. package/dist/use-cases/commands/ooxml-xml-walker.d.ts +36 -0
  90. package/dist/use-cases/commands/option-descriptions.d.ts +5 -0
  91. package/dist/use-cases/commands/output-path.d.ts +23 -0
  92. package/dist/use-cases/commands/pdf-to-markdown.d.ts +19 -0
  93. package/dist/use-cases/commands/pptx-comments.d.ts +24 -0
  94. package/dist/use-cases/commands/pptx-metadata-to-markdown.d.ts +3 -0
  95. package/dist/use-cases/commands/pptx-metadata.d.ts +33 -0
  96. package/dist/use-cases/commands/pptx-slides.d.ts +17 -0
  97. package/dist/use-cases/commands/pptx-to-markdown.d.ts +7 -0
  98. package/dist/use-cases/commands/read-mail-attachment.d.ts +15 -0
  99. package/dist/use-cases/commands/resolve-calendar-link.d.ts +8 -0
  100. package/dist/use-cases/commands/resolve-drive-share-link.d.ts +8 -0
  101. package/dist/use-cases/commands/resolve-mail-link.d.ts +8 -0
  102. package/dist/use-cases/commands/resolve-teams-link.d.ts +8 -0
  103. package/dist/use-cases/commands/search-all-accessible-sites.d.ts +12 -0
  104. package/dist/use-cases/commands/search-index-total.d.ts +3 -0
  105. package/dist/use-cases/commands/search-sharepoint-sites-by-name.d.ts +3 -2
  106. package/dist/use-cases/commands/sharepoint-link-extractor.d.ts +17 -12
  107. package/dist/use-cases/commands/text-passthrough.d.ts +15 -2
  108. package/dist/use-cases/commands/todo-parse-uri-rewrite.d.ts +7 -0
  109. package/dist/use-cases/commands/update-mail-draft.d.ts +22 -0
  110. package/dist/use-cases/commands/version-id.d.ts +7 -6
  111. package/dist/use-cases/commands/xlsx-comments.d.ts +29 -0
  112. package/dist/use-cases/commands/xlsx-metadata-to-markdown.d.ts +3 -0
  113. package/dist/use-cases/commands/xlsx-metadata.d.ts +39 -0
  114. package/dist/use-cases/commands/xlsx-to-markdown.d.ts +9 -2
  115. package/dist/use-cases/commands/zip-archive-to-markdown.d.ts +30 -0
  116. package/dist/use-cases/ports/filesystem.d.ts +10 -0
  117. package/docs/COMMANDS.md +259 -0
  118. package/docs/USAGE.md +266 -0
  119. package/docs/commands.json +1500 -135
  120. package/package.json +16 -8
  121. /package/dist/use-cases/commands/{download-onedrive-file-content.d.ts → download-drive-item-content.d.ts} +0 -0
@@ -31,6 +31,30 @@ type AuthManager = {
31
31
  * via headless Playwright. Used by the 3 historical-version commands.
32
32
  */
33
33
  getElevatedAccessToken: () => Promise<Result<AccessToken, AuthError>>;
34
+ /**
35
+ * Returns a chatsvcagg-audience token (same Teams web client identity
36
+ * as `getAccessToken`, but issued for the chatsvcagg resource). Falls
37
+ * through cache → re-capture via headless Playwright. Used by the
38
+ * `list-teams-chats-with-messages` family of commands.
39
+ */
40
+ getChatsvcaggAccessToken: () => Promise<Result<AccessToken, AuthError>>;
41
+ /**
42
+ * Returns the regional segment used to construct chatsvcagg substrate
43
+ * URLs (`teams.microsoft.com/api/csa/<region>/api/...`). Captured at
44
+ * login from the first such URL the chatsvcagg bearer rides on. Falls
45
+ * back to `DEFAULT_CHATSVCAGG_REGION` ('emea') when the cache is
46
+ * either absent or pre-2026-05-migration. Synchronous on cache; calls
47
+ * `getChatsvcaggAccessToken()` first if no cache exists so a region is
48
+ * available immediately after login.
49
+ */
50
+ getChatsvcaggRegion: () => Promise<string>;
51
+ /**
52
+ * Returns an IC3-audience bearer (Teams web client identity, aud
53
+ * `https://ic3.teams.office.com`). Falls through cache → re-capture
54
+ * via headless Playwright. Used by `list-teams-chat-history` to walk
55
+ * paginated chat-message history beyond the 200-message chatsvcagg cap.
56
+ */
57
+ getIc3AccessToken: () => Promise<Result<AccessToken, AuthError>>;
34
58
  logout: () => Promise<Result<void, AuthError>>;
35
59
  /**
36
60
  * Inspect the elevated-capture outcome from the most recent
@@ -39,13 +63,42 @@ type AuthManager = {
39
63
  * Login-fix round-1 Wave D.
40
64
  */
41
65
  getLastElevatedOutcome: () => ElevatedOutcome | null;
66
+ /**
67
+ * Inspect the chatsvcagg-capture outcome from the most recent
68
+ * `acquireViaBrowser` invocation. Same shape and lifetime semantics
69
+ * as `getLastElevatedOutcome`.
70
+ */
71
+ getLastChatsvcaggOutcome: () => ElevatedOutcome | null;
42
72
  };
43
- declare const createAuthManagerFromApi: (browserAuth: BrowserAuth, cachePath: string, browserProfileDir: string, logger: Logger, fs: FileSystem) => AuthManager;
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) => AuthManager;
85
+ /**
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
88
+ * when a concurrent process refreshes first (AAD rotates SPA refresh tokens,
89
+ * so the loser of the race cannot refresh and falls into the browser leg).
90
+ * Exported for the composition test; pure read, never writes.
91
+ */
92
+ declare const createFreshCachedTokenProbe: (fs: FileSystem, cachePath: string) => (() => Promise<string | null>);
93
+ declare const stderrProgress: (line: string) => void;
44
94
  declare const createAuthManager: (deps: {
45
95
  cachePath: string;
46
96
  logger: Logger;
47
97
  fs?: FileSystem;
48
98
  browserProfileDir?: string;
99
+ systemBrowserAuth?: SystemBrowserAuthFn;
100
+ usePlaywrightFallback?: boolean;
101
+ skipSystemBrowser?: boolean;
49
102
  }) => AuthManager;
50
- export { createAuthManager, createAuthManagerFromApi };
51
- export type { AuthError, AuthManager, ElevatedOutcome };
103
+ export { createAuthManager, createAuthManagerFromApi, createFreshCachedTokenProbe, stderrProgress };
104
+ export type { AuthError, AuthManager, ElevatedOutcome, SystemBrowserAuthFn };
@@ -26,19 +26,70 @@ type ElevatedTokenResult = {
26
26
  readonly reason: ElevatedFailureReason;
27
27
  };
28
28
  /**
29
- * Combined outcome of capturing BOTH the Teams web client token AND the
30
- * elevated M365ChatClient token inside one browser session. Login-fix
31
- * round-2 introduced this to fix the user-visible "second browser asks
32
- * me to log in again" symptom on federated tenants (LVMH / Okta): the
33
- * old flow opened two separate browser sessions, and silent SSO for the
34
- * elevated identity wouldn't pick up the cookies just freshly written
35
- * by the first session. Capturing both in one context means cookies are
36
- * live in memory — no disk-commit-vs-read race, no separate sign-in
37
- * prompt for the elevated identity.
29
+ * chatsvcagg capture result. Carries the same three failure modes as
30
+ * `ElevatedTokenResult` PLUS the parsed regional segment from the URL
31
+ * the bearer rode on (`emea` / `amer` / `apac` / etc.) — the new
32
+ * substrate host (`teams.microsoft.com/api/csa/<region>/api/...`) routes
33
+ * per region, so callers must persist this alongside the token to
34
+ * construct future URLs. See `gotcha_chatsvcagg_substrate_moved` in
35
+ * the project memory for the 2026-05 migration that made this
36
+ * necessary.
37
+ */
38
+ type ChatsvcaggTokenResult = {
39
+ readonly ok: true;
40
+ readonly token: AccessToken;
41
+ readonly region: string;
42
+ } | {
43
+ readonly ok: false;
44
+ readonly reason: ElevatedFailureReason;
45
+ };
46
+ /**
47
+ * IC3 capture result. The "next-gen" Teams chat-message substrate at
48
+ * `teams.microsoft.com/api/chatsvc/<region>/v1/users/ME/conversations/{id}/messages`
49
+ * — the path Teams web actually uses for chat scrollback (the existing
50
+ * chatsvcagg substrate at `/api/csa/<region>/api/v1/chats/{id}/messages`
51
+ * is the chat-list aggregator and caps at the 200 most recent messages
52
+ * with no working pagination cursor). The IC3 path supports `syncState`
53
+ * + `startTime` pagination, unlocking arbitrary-depth chat history.
54
+ *
55
+ * Bearer audience: `https://ic3.teams.office.com`. Standard `Bearer`
56
+ * authorization (NOT the purple-teams-documented `skype_token X`
57
+ * header — those forks predate Teams' move to OAuth bearer on this
58
+ * surface). Empirically discovered 2026-05-21 by widening the
59
+ * Playwright capture listener.
60
+ */
61
+ type Ic3TokenResult = {
62
+ readonly ok: true;
63
+ readonly token: AccessToken;
64
+ readonly region: string;
65
+ } | {
66
+ readonly ok: false;
67
+ readonly reason: ElevatedFailureReason;
68
+ };
69
+ /**
70
+ * Combined outcome of capturing all four tokens (Teams basic / M365
71
+ * elevated / chatsvcagg substrate / IC3 substrate) inside one browser
72
+ * session. Login-fix round-2 introduced this for the basic+elevated
73
+ * pair; chatsvcagg was added next; IC3 is the most recent leg, capturing
74
+ * the bearer Teams web uses for unbounded chat-history reads.
75
+ *
76
+ * All four legs ride on the SAME `teams.microsoft.com` session, so
77
+ * adding capture legs is essentially free — MSAL multiplexes the
78
+ * audience-scoped bearers via silent acquisition.
38
79
  */
39
80
  type BothTokensResult = {
40
81
  readonly teams: BrowserTokenResult | null;
41
82
  readonly elevated: ElevatedTokenResult;
83
+ readonly chatsvcagg: ChatsvcaggTokenResult;
84
+ readonly ic3: Ic3TokenResult;
85
+ /**
86
+ * Set when the Teams poll short-circuited because `freshCachedToken`
87
+ * found a token written by a concurrent process (QA-010). The caller
88
+ * must NOT persist this result — the cache is already the source of
89
+ * truth, and `refreshToken` is null here (persisting would clobber the
90
+ * winner's rotated refresh token).
91
+ */
92
+ readonly fromCache?: true;
42
93
  };
43
94
  type BrowserAuth = {
44
95
  acquireToken: (scopes: string[], startUrl: string) => Promise<BrowserTokenResult | null>;
@@ -62,6 +113,28 @@ type BrowserAuth = {
62
113
  * `acquireBothTokens` instead so the user sees only one browser.
63
114
  */
64
115
  acquireElevatedToken: () => Promise<ElevatedTokenResult>;
116
+ /**
117
+ * Capture a chatsvcagg-audience bearer (Teams basic identity, but the
118
+ * `chatsvcagg.teams.microsoft.com` resource instead of Graph) by
119
+ * navigating headless to `teams.microsoft.com/v2/`. The persistent
120
+ * profile's SSO cookies do the auth silently. Used by
121
+ * `getChatsvcaggAccessToken()` when the cached chatsvcagg token has
122
+ * expired and the basic Teams session is still warm.
123
+ *
124
+ * Same failure-mode shape as `acquireElevatedToken` — launch hang,
125
+ * navigation failure, silent SSO timeout — so the auth-manager can
126
+ * reuse the same auto-heal logic.
127
+ */
128
+ acquireChatsvcaggToken: () => Promise<ChatsvcaggTokenResult>;
129
+ /**
130
+ * Capture an IC3-audience bearer (aud `https://ic3.teams.office.com`,
131
+ * same basic Teams appid). The bearer Teams web client uses to call
132
+ * `teams.microsoft.com/api/chatsvc/<region>/v1/users/ME/conversations/{id}/messages`
133
+ * — the chat-message substrate with proper `syncState` pagination,
134
+ * enabling reads beyond the 200-message chatsvcagg cap. Same failure
135
+ * modes as `acquireChatsvcaggToken`.
136
+ */
137
+ acquireIc3Token: () => Promise<Ic3TokenResult>;
65
138
  /**
66
139
  * Login-fix round-2: capture BOTH tokens inside ONE browser session.
67
140
  * After the Teams response listener intercepts the Teams token,
@@ -125,6 +198,21 @@ type BrowserAuthConfig = {
125
198
  readonly logger: Logger;
126
199
  readonly fs: FileSystem;
127
200
  readonly trace?: TraceFn;
201
+ /**
202
+ * QA-010 (login hang): probe for a fresh token that landed in the cache
203
+ * WHILE the browser capture is polling. AAD SPA refresh tokens rotate, so
204
+ * when two processes race, the loser's refresh fails and it falls into the
205
+ * full multi-minute browser dance — while the winner's fresh token sits in
206
+ * the cache the whole time. The Teams poll loop calls this every interval
207
+ * and short-circuits (closing the browser) the moment it returns a token.
208
+ */
209
+ readonly freshCachedToken?: () => Promise<string | null>;
210
+ /**
211
+ * User-visible progress sink (stderr in production). The browser capture
212
+ * can legitimately take minutes (federated SSO, interactive sign-in); these
213
+ * one-liners are what separates "waiting on the user" from "hung" (QA-010).
214
+ */
215
+ readonly onProgress?: (line: string) => void;
128
216
  readonly profileDir?: string;
129
217
  readonly initialSettleMs?: number;
130
218
  readonly postReloginSettleMs?: number;
@@ -158,6 +246,8 @@ declare const createBrowserAuthFromApi: (api: BrowserAuthApi, config: BrowserAut
158
246
  declare const createBrowserAuth: (deps: {
159
247
  logger: Logger;
160
248
  fs?: FileSystem;
249
+ freshCachedToken?: () => Promise<string | null>;
250
+ onProgress?: (line: string) => void;
161
251
  }) => BrowserAuth;
162
252
  export { createBrowserAuth, createBrowserAuthFromApi, createPlaywrightApi };
163
- export type { BothTokensResult, BrowserAuth, BrowserAuthApi, BrowserAuthConfig, BrowserTokenResult, ChromiumLike, ContextLike, ElevatedFailureReason, ElevatedTokenResult, PageLike, PlaywrightLoader, RequestLike, ResponseLike, };
253
+ export type { BothTokensResult, BrowserAuth, BrowserAuthApi, BrowserAuthConfig, BrowserTokenResult, ChatsvcaggTokenResult, Ic3TokenResult, ChromiumLike, ContextLike, ElevatedFailureReason, ElevatedTokenResult, PageLike, PlaywrightLoader, RequestLike, ResponseLike, };
@@ -5,6 +5,7 @@ type GraphError = {
5
5
  status: number;
6
6
  message: string;
7
7
  code?: string;
8
+ retryAfterSeconds?: number;
8
9
  } | {
9
10
  type: 'auth_failed';
10
11
  message: string;
@@ -35,7 +36,38 @@ type GraphClient = {
35
36
  * elevated token).
36
37
  */
37
38
  getElevated: (path: string) => Promise<Result<unknown, GraphError>>;
39
+ /**
40
+ * JSON-GET against the Teams chat substrate (post-2026-05:
41
+ * `teams.microsoft.com/api/csa/<region>/api/v{N}/...` — see
42
+ * `gotcha_chatsvcagg_substrate_moved` in memory for the migration
43
+ * away from `chatsvcagg.teams.microsoft.com`). Signs the request
44
+ * with the chatsvcagg-audience bearer captured at login (same Teams
45
+ * web client identity as `get`, different audience), and injects the
46
+ * cached substrate region between the host and `path`. Used by
47
+ * commands that need to read chat message BODIES, which the basic
48
+ * Graph token cannot reach (`Chat.Read*` scopes are missing).
49
+ *
50
+ * `path` MUST start with `/api/v{N}/...` — the host + `/api/csa/<region>`
51
+ * prefix are added by this client.
52
+ */
53
+ teamsChat: (path: string) => Promise<Result<unknown, GraphError>>;
54
+ /**
55
+ * JSON-GET against the Teams IC3 chat-message substrate at
56
+ * `teams.microsoft.com/api/chatsvc/<region>/v1/...`. Same host as
57
+ * `teamsChat` but a DIFFERENT path prefix AND a different bearer
58
+ * audience (`https://ic3.teams.office.com` instead of
59
+ * `https://chatsvcagg.teams.microsoft.com`). The path supports
60
+ * `syncState` + `startTime` pagination — unlocking arbitrary-depth
61
+ * chat-history reads beyond the chatsvcagg 200-message cap (see
62
+ * `gotcha_chatsvcagg_substrate_moved` in memory). Used by
63
+ * `list-teams-chat-history`.
64
+ *
65
+ * `path` MUST start with `/v1/...` (e.g. `/v1/users/ME/conversations/{id}/messages?startTime=...`)
66
+ * — the host + `/api/chatsvc/<region>` prefix are added here.
67
+ */
68
+ teamsChatIc3: (path: string) => Promise<Result<unknown, GraphError>>;
38
69
  post: (path: string, body: unknown) => Promise<Result<unknown, GraphError>>;
70
+ patch: (path: string, body: unknown) => Promise<Result<unknown, GraphError>>;
39
71
  getBinary: (path: string) => Promise<Result<unknown, GraphError>>;
40
72
  /**
41
73
  * Same shape as `getBinary` but signs the request with an "elevated"
@@ -74,6 +106,14 @@ type TokenInfo = {
74
106
  readonly scopes: ReadonlyArray<string>;
75
107
  readonly audience: string | undefined;
76
108
  readonly expiresAt: string | undefined;
109
+ /**
110
+ * Seconds remaining until the cached token's `exp` claim — derived from
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
114
+ * worth doing under ~5 minutes) without parsing the ISO string itself.
115
+ */
116
+ readonly expiresInSeconds: number | undefined;
77
117
  };
78
118
  type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
79
119
  declare const createGraphClient: (auth: AuthManager, fetchFn?: FetchFn) => GraphClient;
@@ -0,0 +1,14 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { GraphError } from './graph-client.js';
3
+ /**
4
+ * Extract the text of a legacy Word .doc — the pre-2007 OLE binary "Compound
5
+ * File" format that mammoth (which only reads the OOXML .docx zip) cannot parse —
6
+ * via word-extractor (pure-JS, walks the WordDocument OLE stream; no native deps).
7
+ * Body text only: legacy .doc carries no structure this CLI surfaces (headings,
8
+ * tables, and styling are lost), so callers return it as text/plain.
9
+ *
10
+ * try/catch is permitted here per the infra-boundary rule: word-extractor throws
11
+ * on a non-.doc / corrupt OLE container, and we translate that into a Result.
12
+ */
13
+ declare const extractDocText: (bytes: Uint8Array) => Promise<Result<string, GraphError>>;
14
+ export { extractDocText };
@@ -0,0 +1,83 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { GraphError } from './graph-client.js';
3
+ /**
4
+ * Parse an Outlook `.msg` file — the OLE Compound File (CFBF) container Outlook
5
+ * writes when you drag an email to disk, same binary family as legacy .doc/.xls —
6
+ * into a clean {@link ParsedMsg} via @kenjiuno/msgreader (pure-JS, no native deps,
7
+ * lazy-imported like word-extractor / unpdf so it stays out of the cold-start path).
8
+ *
9
+ * The adapter is the anti-corruption layer: msgreader returns a wide `FieldsData`
10
+ * (X500 sender addresses, RTF body, hundreds of MAPI props); {@link mapRawMsg}
11
+ * narrows it to the half-dozen fields the markdown renderer needs and resolves the
12
+ * sender SMTP / delivery-time fallbacks. `mapRawMsg` is pure and exported so every
13
+ * fallback branch is unit-tested with plain objects — the binary fixture only
14
+ * exercises one field combination.
15
+ *
16
+ * try/catch is permitted here per the infra-boundary rule: msgreader throws on a
17
+ * non-OLE / corrupt container, and a single unreadable attachment must not sink the
18
+ * whole parse, so attachment reads are individually guarded.
19
+ */
20
+ type MsgRecipientKind = 'to' | 'cc' | 'bcc' | 'unknown';
21
+ type MsgRecipient = {
22
+ readonly kind: MsgRecipientKind;
23
+ readonly name?: string;
24
+ readonly email?: string;
25
+ };
26
+ type MsgAttachment = {
27
+ readonly fileName?: string;
28
+ readonly content?: Uint8Array;
29
+ };
30
+ type ParsedMsg = {
31
+ readonly subject?: string;
32
+ readonly senderName?: string;
33
+ readonly senderEmail?: string;
34
+ readonly date?: string;
35
+ readonly body?: string;
36
+ readonly bodyHtml?: string;
37
+ readonly recipients: readonly MsgRecipient[];
38
+ readonly attachments: readonly MsgAttachment[];
39
+ };
40
+ type RawRecipient = {
41
+ readonly recipType?: string;
42
+ readonly name?: string;
43
+ readonly smtpAddress?: string;
44
+ readonly email?: string;
45
+ };
46
+ type RawAttachment = {
47
+ readonly fileName?: string;
48
+ };
49
+ type RawMsg = {
50
+ readonly subject?: string;
51
+ readonly senderName?: string;
52
+ readonly senderSmtpAddress?: string;
53
+ readonly senderEmail?: string;
54
+ readonly messageDeliveryTime?: string;
55
+ readonly clientSubmitTime?: string;
56
+ readonly body?: string;
57
+ readonly bodyHtml?: string;
58
+ readonly recipients?: readonly RawRecipient[];
59
+ readonly attachments?: readonly RawAttachment[];
60
+ };
61
+ declare const mapRawMsg: (raw: RawMsg, attachmentContents: readonly (Uint8Array | undefined)[]) => ParsedMsg;
62
+ type AttachmentReader = {
63
+ readonly getAttachment: (index: number) => {
64
+ readonly content?: unknown;
65
+ };
66
+ };
67
+ type MsgReaderInstance = AttachmentReader & {
68
+ readonly getFileData: () => unknown;
69
+ };
70
+ type MsgReaderCtor = new (input: ArrayBuffer | DataView) => MsgReaderInstance;
71
+ declare const readAttachmentContent: (reader: AttachmentReader, index: number) => Uint8Array | undefined;
72
+ /**
73
+ * Resolve the MsgReader class from the dynamic import's `default`, whichever
74
+ * interop shape it arrives in. msgreader is CJS with `exports.default = class`
75
+ * (+ `__esModule`): Bun's RUNTIME import hands us the class directly, but Bun's
76
+ * BUNDLER (node-mode `__toESM`) sets `default` to the whole exports object —
77
+ * so in `dist/cli.js` the class sits one level deeper at `default.default`
78
+ * ("Object is not a constructor" at runtime, invisible to source-run tests).
79
+ */
80
+ declare const resolveMsgReaderCtor: (defaultExport: unknown) => MsgReaderCtor;
81
+ declare const extractMsg: (bytes: Uint8Array) => Promise<Result<ParsedMsg, GraphError>>;
82
+ export { extractMsg, mapRawMsg, readAttachmentContent, resolveMsgReaderCtor };
83
+ export type { MsgAttachment, MsgRecipient, MsgRecipientKind, ParsedMsg };
@@ -0,0 +1,9 @@
1
+ export declare const REQUEST_TIMEOUT_MS = 60000;
2
+ export declare const BINARY_TRANSFER_TIMEOUT_MS: number;
3
+ export declare const REQUEST_TIMEOUT_LABEL = "60s";
4
+ export declare const BINARY_TRANSFER_TIMEOUT_LABEL = "5min";
5
+ export declare const networkErrorMessage: (e: unknown, label: string, timeoutLabel: string) => string;
6
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
7
+ export type TimeoutTier = 'json' | 'binary';
8
+ export declare const timeoutLabelFor: (tier: TimeoutTier) => string;
9
+ export declare const timeoutMsFor: (tier: TimeoutTier) => number;
@@ -0,0 +1,27 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { GraphError } from './graph-client.js';
3
+ /**
4
+ * Pulls the image parts out of an OOXML package (.docx / .xlsx /
5
+ * .pptx and their macro-enabled / template variants). Images live as binary
6
+ * parts under `word/media/`, `xl/media/`, or `ppt/media/` — including
7
+ * original full-resolution / un-cropped originals and images on hidden
8
+ * slides that the rendered view never shows.
9
+ *
10
+ * Separate from `ooxml-zip-adapter` on purpose: that adapter pre-decodes
11
+ * every entry as a UTF-8 string (correct for XML, corrupting for binary),
12
+ * whereas media must come back as raw bytes. try/catch is permitted here
13
+ * (src/infra/**, atelier rule 17): a malformed-zip throw becomes a Result.err.
14
+ *
15
+ * Raster formats (png/jpg/gif/bmp/tiff/webp) a vision model can read directly,
16
+ * plus svg — the one vector format a model can use, because its XML source
17
+ * carries the diagram's own text labels (and modern Office embeds it as the
18
+ * high-fidelity original beside a png fallback). Legacy vector (emf/wmf) and
19
+ * audio/video are still excluded — neither a model can use.
20
+ */
21
+ type MediaPart = {
22
+ readonly path: string;
23
+ readonly bytes: Uint8Array;
24
+ };
25
+ declare const extractOoxmlMedia: (bytes: Uint8Array) => Promise<Result<ReadonlyArray<MediaPart>, GraphError>>;
26
+ export { extractOoxmlMedia };
27
+ export type { MediaPart };
@@ -0,0 +1,28 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { GraphError } from './graph-client.js';
3
+ /**
4
+ * Thin wrapper around JSZip for opening already-fetched OOXML bytes
5
+ * (.docx / .xlsx / .pptx — all ZIP packages with a shared docProps/* core).
6
+ * Pure CPU on bytes already in memory — no IO — so this matches the
7
+ * mammoth-adapter pattern: a Result-returning factory, no port, no fake.
8
+ * Tests work end-to-end against real fixtures built with the `docx` /
9
+ * `xlsx` packages (or hand-rolled JSZip) via test-helpers/office-fixtures.ts.
10
+ *
11
+ * try/catch is permitted under src/infra/** (atelier hard rule 17): any
12
+ * malformed-zip throw from JSZip translates into a Result.err with a
13
+ * GraphError shape so callers stay on the Result rail.
14
+ *
15
+ * The returned reader is synchronous on purpose: callers in the use-case
16
+ * layer are pure XML walkers that don't model async/await internally.
17
+ * Every text entry is pre-decoded once at open time, then served from
18
+ * an in-memory map. `list()` exposes the entry paths so callers can
19
+ * enumerate numbered parts (`xl/comments1.xml`, `ppt/slides/slide*.xml`,
20
+ * every `*.rels`) that can't be hardcoded.
21
+ */
22
+ type OoxmlZip = {
23
+ readonly read: (path: string) => string | undefined;
24
+ readonly list: () => ReadonlyArray<string>;
25
+ };
26
+ declare const openOoxmlZip: (bytes: Uint8Array) => Promise<Result<OoxmlZip, GraphError>>;
27
+ export { openOoxmlZip };
28
+ export type { OoxmlZip };
@@ -0,0 +1,18 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { GraphError } from './graph-client.js';
3
+ import type { MediaPart } from './ooxml-media-extractor.js';
4
+ /**
5
+ * Extract embedded raster images from a PDF via unpdf (a pure-JS pdfjs build — no
6
+ * native deps, runs under Bun). unpdf walks each page's painted image XObjects and
7
+ * returns *decoded* pixels, which we PNG-encode.
8
+ *
9
+ * Scope note: this is page-oriented — it sees images AS PAINTED on each page. It does
10
+ * NOT reach OCG/layer-hidden images, unpainted/orphan XObjects, or the full uncropped
11
+ * original behind a clipped image; capturing those needs an object-graph walk (a much
12
+ * heavier dependency). All PAGES are always walked (PDF has no "hidden page" concept).
13
+ *
14
+ * `try/catch` is permitted here per the infra-boundary rule: pdfjs throws on malformed
15
+ * input and we translate that into a Result rather than letting it escape.
16
+ */
17
+ declare const extractPdfImages: (bytes: Uint8Array) => Promise<Result<ReadonlyArray<MediaPart>, GraphError>>;
18
+ export { extractPdfImages };
@@ -0,0 +1,41 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { GraphError } from './graph-client.js';
3
+ /**
4
+ * Extract a PDF's text layer via unpdf (a pure-JS pdfjs build — no native deps,
5
+ * runs under Bun). Returns the reading-order text of every page merged into one
6
+ * string. A born-digital PDF (exported from Word/LaTeX/Chrome/…) yields its text;
7
+ * a scanned / image-only PDF has no text layer, so the string comes back empty —
8
+ * callers treat that as "needs OCR / a vision model", never as success-with-text.
9
+ *
10
+ * Scope note: this is the embedded text layer, NOT OCR. It also does not preserve
11
+ * layout structure (headings, tables, columns flatten into reading order).
12
+ *
13
+ * `try/catch` is permitted here per the infra-boundary rule: pdfjs throws on
14
+ * malformed input and we translate that into a Result rather than letting it escape.
15
+ *
16
+ * `verbosity: 0` (pdfjs `VerbosityLevel.ERRORS`) silences pdfjs's TrueType
17
+ * font-hinting log spam — `Warning: TT: undefined function: N`, `Required "glyf"
18
+ * table is not found`, `Indexing all PDF objects` — which it emits via its own
19
+ * `console.warn` while parsing a PDF's embedded fonts. Font hinting is sub-pixel
20
+ * glyph rendering, completely irrelevant to text extraction (the text still comes
21
+ * out fine); the default level (WARNINGS) just floods the host app's logs once per
22
+ * quirky font. We lower it at the boundary because the noise originates inside
23
+ * pdfjs — the `no-console` rule keeps us from emitting or intercepting it ourselves.
24
+ */
25
+ /**
26
+ * Map a pdfjs throw to an actionable message. The special case is an encrypted
27
+ * PDF: pdfjs raises `PasswordException` ("No password given") — QA run-1 found a
28
+ * real tenant file surfacing that raw string with zero guidance. Password
29
+ * handling is genuinely unsupported (and Graph's `format=pdf` cannot unlock
30
+ * encrypted sources either), so say that honestly instead of leaking pdfjs
31
+ * internals. Pure and exported: the branch is unit-tested with shaped errors —
32
+ * no encrypted-PDF fixture exists in the toolchain.
33
+ */
34
+ declare const pdfErrorMessage: (e: unknown) => string;
35
+ type PdfText = {
36
+ readonly text: string;
37
+ readonly pageCount: number;
38
+ };
39
+ declare const extractPdfText: (bytes: Uint8Array) => Promise<Result<PdfText, GraphError>>;
40
+ export { extractPdfText, pdfErrorMessage };
41
+ export type { PdfText };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal, dependency-free PNG encoder for raw 8-bit pixel buffers. unpdf hands
3
+ * back *decoded* pixels (1=grayscale, 3=RGB, 4=RGBA channels), so to write usable
4
+ * image files without a native dep (sharp/canvas) we wrap them in a PNG ourselves:
5
+ * signature + IHDR + a single zlib-compressed IDAT (filter-0 scanlines) + IEND.
6
+ */
7
+ type RawImage = {
8
+ readonly width: number;
9
+ readonly height: number;
10
+ readonly channels: number;
11
+ readonly data: Uint8Array;
12
+ };
13
+ declare const encodePng: (image: RawImage) => Uint8Array;
14
+ export { encodePng };
15
+ export type { RawImage };
@@ -4,6 +4,6 @@ type SheetCsv = {
4
4
  readonly name: string;
5
5
  readonly csv: string;
6
6
  };
7
- declare const readSheetsAsCsv: (bytes: Uint8Array) => Result<ReadonlyArray<SheetCsv>, GraphError>;
7
+ declare const readSheetsAsCsv: (bytes: Uint8Array) => Promise<Result<ReadonlyArray<SheetCsv>, GraphError>>;
8
8
  export { readSheetsAsCsv };
9
9
  export type { SheetCsv };
@@ -0,0 +1,35 @@
1
+ import type { AccessToken } from '../domain/access-token.js';
2
+ import type { Result } from '../domain/result.js';
3
+ import type { Logger } from '../use-cases/ports/logger.js';
4
+ type SystemBrowserAuthDeps = {
5
+ readonly logger: Logger;
6
+ readonly timeoutMs?: number;
7
+ readonly extensionTimeoutMs?: number;
8
+ };
9
+ type SystemBrowserTokenResult = {
10
+ readonly accessToken: AccessToken;
11
+ readonly refreshToken: string | null;
12
+ readonly elevatedAccessToken?: AccessToken | null;
13
+ readonly chatsvcaggAccessToken?: AccessToken | null;
14
+ readonly ic3AccessToken?: AccessToken | null;
15
+ readonly chatsvcaggRegion?: string;
16
+ };
17
+ type SystemBrowserAuthError = {
18
+ type: 'server_bind_failed';
19
+ message: string;
20
+ } | {
21
+ type: 'browser_open_failed';
22
+ message: string;
23
+ } | {
24
+ type: 'extension_timeout';
25
+ message: string;
26
+ } | {
27
+ type: 'invalid_token';
28
+ message: string;
29
+ } | {
30
+ type: 'cancelled';
31
+ message: string;
32
+ };
33
+ declare const authenticateViaSystemBrowser: (deps: SystemBrowserAuthDeps) => Promise<Result<SystemBrowserTokenResult, SystemBrowserAuthError>>;
34
+ export { authenticateViaSystemBrowser };
35
+ export type { SystemBrowserAuthDeps, SystemBrowserAuthError, SystemBrowserTokenResult };
@@ -0,0 +1,3 @@
1
+ import type { Logger } from '../use-cases/ports/logger.js';
2
+ import type { SystemBrowserAuthFn } from './auth.js';
3
+ export declare const defaultSystemBrowserAuth: (logger: Logger, skipSystemBrowser: boolean) => SystemBrowserAuthFn;
@@ -0,0 +1,31 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { Logger } from '../use-cases/ports/logger.js';
3
+ type TokenCallbackPayload = {
4
+ readonly access_token: string;
5
+ readonly refresh_token?: string;
6
+ readonly elevated_access_token?: string;
7
+ readonly chatsvcagg_access_token?: string;
8
+ readonly ic3_access_token?: string;
9
+ readonly chatsvcagg_region?: string;
10
+ };
11
+ type TokenCallbackServer = {
12
+ readonly port: number;
13
+ readonly start: () => Promise<Result<TokenCallbackPayload, TokenCallbackError>>;
14
+ readonly stop: () => Promise<void>;
15
+ };
16
+ type TokenCallbackError = {
17
+ type: 'bind_failed';
18
+ message: string;
19
+ } | {
20
+ type: 'timeout';
21
+ message: string;
22
+ } | {
23
+ type: 'invalid_payload';
24
+ message: string;
25
+ } | {
26
+ type: 'server_closed';
27
+ message: string;
28
+ };
29
+ declare const createTokenCallbackServer: (logger: Logger, timeoutMs?: number) => TokenCallbackServer;
30
+ export { createTokenCallbackServer };
31
+ export type { TokenCallbackError, TokenCallbackPayload, TokenCallbackServer };
@@ -0,0 +1,32 @@
1
+ import type { Result } from '../domain/result.js';
2
+ import type { GraphError } from './graph-client.js';
3
+ /**
4
+ * Reads every file entry out of an arbitrary `.zip` archive as raw bytes,
5
+ * sorted by path, directories excluded. Generic counterpart to
6
+ * `ooxml-media-extractor` (which matches only media paths) and
7
+ * `ooxml-zip-adapter` (which decodes entries as UTF-8 strings) — used by the
8
+ * zip-conversion commands to fan each contained file out to the right
9
+ * converter. try/catch is permitted here (src/infra/**, atelier rule 17): a
10
+ * malformed-zip throw becomes a Result.err.
11
+ */
12
+ type ZipEntry = {
13
+ readonly path: string;
14
+ readonly bytes: Uint8Array;
15
+ };
16
+ /**
17
+ * Decode a legacy zip entry name — the raw filename bytes of an entry whose
18
+ * UTF-8 flag (general-purpose bit 11) is NOT set and which carries no Info-ZIP
19
+ * Unicode-path extra field. JSZip's default decoder assumes UTF-8 and mojibakes
20
+ * these (the `unzip -O GBK` case): Chinese vendor archives written by WinRAR /
21
+ * Windows Explorer store names in GBK. Try strict UTF-8 first (some archivers
22
+ * emit UTF-8 bytes without setting the flag), then fall back to GB18030 — a GBK
23
+ * superset whose 0x00–0x7F bytes are plain ASCII, so legacy ASCII/CP437 names
24
+ * survive unchanged while CJK byte sequences decode correctly.
25
+ *
26
+ * Only reached for non-UTF-8-flagged names (JSZip handles UTF-8 names itself),
27
+ * so a UTF-8 archive's path-handling is unchanged.
28
+ */
29
+ declare const decodeZipFileName: (bytes: Uint8Array) => string;
30
+ declare const openZipEntries: (bytes: Uint8Array) => Promise<Result<ReadonlyArray<ZipEntry>, GraphError>>;
31
+ export { decodeZipFileName, openZipEntries };
32
+ export type { ZipEntry };