@wirecat/cli-core 0.562.0 → 0.564.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 (142) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/config-command.js +18 -1
  4. package/dist/cli/envelope.js +13 -1
  5. package/dist/cli/execution.js +1 -0
  6. package/dist/cli/messenger/archive-commands.js +17 -0
  7. package/dist/cli/messenger/catch-up-answers.d.ts +5 -0
  8. package/dist/cli/messenger/catch-up-answers.js +3 -0
  9. package/dist/cli/messenger/reply-stats-command.d.ts +4 -0
  10. package/dist/cli/messenger/reply-stats-command.js +57 -0
  11. package/dist/cli/messenger/search-answers.d.ts +1 -0
  12. package/dist/cli/messenger/search-answers.js +1 -1
  13. package/dist/cli/messenger/search-command.js +8 -0
  14. package/dist/cli/messenger/stats-answers.d.ts +6 -3
  15. package/dist/cli/messenger/stats-answers.js +39 -9
  16. package/dist/cli/messenger/stats-command.js +5 -0
  17. package/dist/cli/messenger/tasks-command.js +35 -14
  18. package/dist/cli/output-budget.d.ts +26 -0
  19. package/dist/cli/output-budget.js +73 -0
  20. package/dist/cli/program.js +14 -1
  21. package/dist/cli/settings.d.ts +5 -0
  22. package/dist/cli/settings.js +88 -5
  23. package/dist/cli/shared-command-exports.d.ts +8 -0
  24. package/dist/conversations/mail-text.d.ts +6 -0
  25. package/dist/conversations/mail-text.js +73 -0
  26. package/dist/domain/admin-statistics.d.ts +5 -0
  27. package/dist/domain/admin-statistics.js +4 -3
  28. package/dist/domain/calendar.d.ts +2 -0
  29. package/dist/domain/calendar.js +6 -0
  30. package/dist/entities/chat-stats.d.ts +16 -2
  31. package/dist/entities/chat-stats.js +18 -3
  32. package/dist/entities/index.d.ts +80 -802
  33. package/dist/entities/index.js +26 -16
  34. package/dist/entities/mail-counts.d.ts +15 -0
  35. package/dist/entities/mail-counts.js +16 -0
  36. package/dist/entities/meeting.d.ts +1 -1
  37. package/dist/entities/meeting.js +1 -1
  38. package/dist/entities/reply-stats.d.ts +33 -0
  39. package/dist/entities/reply-stats.js +19 -0
  40. package/dist/entities/store-records.d.ts +8 -0
  41. package/dist/entities/store-records.js +8 -0
  42. package/dist/errors/reasons.d.ts +1 -1
  43. package/dist/errors/reasons.js +7 -3
  44. package/dist/mail/attachments.d.ts +15 -2
  45. package/dist/mail/attachments.js +19 -3
  46. package/dist/mail/cli/index.d.ts +22 -1
  47. package/dist/mail/cli/index.js +28 -4
  48. package/dist/mail/cli/review.d.ts +5 -2
  49. package/dist/mail/cli/review.js +58 -43
  50. package/dist/mail/index.d.ts +4 -0
  51. package/dist/mail/index.js +4 -0
  52. package/dist/mail/partial.d.ts +3 -1
  53. package/dist/mail/partial.js +2 -2
  54. package/dist/mail/retry.d.ts +3 -1
  55. package/dist/mail/retry.js +3 -3
  56. package/dist/mail/stats.d.ts +50 -0
  57. package/dist/mail/stats.js +60 -0
  58. package/dist/mail/sync-types.d.ts +3 -0
  59. package/dist/mail/sync.js +5 -3
  60. package/dist/mail/whole.d.ts +28 -0
  61. package/dist/mail/whole.js +70 -0
  62. package/dist/meetings/index.d.ts +2 -2
  63. package/dist/meetings/index.js +2 -2
  64. package/dist/meetings/mcp/index.d.ts +15 -15
  65. package/dist/meetings/read-answers.d.ts +22 -1
  66. package/dist/meetings/read-answers.js +38 -0
  67. package/dist/meetings/stats.d.ts +44 -0
  68. package/dist/meetings/stats.js +77 -7
  69. package/dist/search/lucene/registry.js +2 -1
  70. package/dist/search/question-plan.d.ts +16 -0
  71. package/dist/search/question-plan.js +34 -0
  72. package/dist/services/events-search.d.ts +5 -1
  73. package/dist/services/events-search.js +10 -4
  74. package/dist/services/index.d.ts +3 -1
  75. package/dist/services/index.js +3 -1
  76. package/dist/services/meeting-stats.d.ts +12 -1
  77. package/dist/services/meeting-stats.js +12 -0
  78. package/dist/services/messages-discovery.d.ts +1 -5
  79. package/dist/services/messages-discovery.js +5 -18
  80. package/dist/services/messages-meaning.js +25 -3
  81. package/dist/services/messages.js +4 -3
  82. package/dist/services/notes-search.d.ts +5 -1
  83. package/dist/services/notes-search.js +16 -5
  84. package/dist/services/proposal-tasks.js +3 -12
  85. package/dist/services/proposals/ids.d.ts +2 -2
  86. package/dist/services/proposals/ids.js +5 -3
  87. package/dist/services/proposals/kinds/action.js +3 -2
  88. package/dist/services/proposals/kinds/findings.d.ts +1 -1
  89. package/dist/services/proposals/kinds/findings.js +1 -1
  90. package/dist/services/proposals/kinds/meeting-findings.d.ts +7 -0
  91. package/dist/services/proposals/kinds/meeting-findings.js +73 -0
  92. package/dist/services/proposals/kinds/task-close.d.ts +7 -0
  93. package/dist/services/proposals/kinds/task-close.js +76 -0
  94. package/dist/services/proposals/registry.js +12 -4
  95. package/dist/services/proposals/types.d.ts +2 -0
  96. package/dist/services/reply-stats.d.ts +48 -0
  97. package/dist/services/reply-stats.js +137 -0
  98. package/dist/services/search-all.d.ts +5 -0
  99. package/dist/services/search-all.js +47 -14
  100. package/dist/services/task-stats.d.ts +41 -0
  101. package/dist/services/task-stats.js +126 -0
  102. package/dist/services/tasks.d.ts +17 -4
  103. package/dist/services/tasks.js +31 -14
  104. package/dist/store/chats.d.ts +3 -1
  105. package/dist/store/chats.js +6 -0
  106. package/dist/store/decisions.d.ts +16 -11
  107. package/dist/store/decisions.js +11 -2
  108. package/dist/store/exports.d.ts +1 -1
  109. package/dist/store/exports.js +1 -1
  110. package/dist/store/index.d.ts +5 -4
  111. package/dist/store/index.js +3 -3
  112. package/dist/store/queries.d.ts +4 -0
  113. package/dist/store/queries.js +3 -0
  114. package/dist/store/sqlite/completeness.d.ts +17 -0
  115. package/dist/store/sqlite/completeness.js +49 -13
  116. package/dist/store/sqlite/email-locations.d.ts +4 -0
  117. package/dist/store/sqlite/email-locations.js +26 -11
  118. package/dist/store/sqlite/lucene-fields.d.ts +2 -0
  119. package/dist/store/sqlite/lucene-fields.js +5 -1
  120. package/dist/store/sqlite/lucene-groups.d.ts +15 -0
  121. package/dist/store/sqlite/lucene-groups.js +21 -0
  122. package/dist/store/sqlite/lucene.d.ts +2 -9
  123. package/dist/store/sqlite/lucene.js +1 -12
  124. package/dist/store/sqlite/mail-findings.js +1 -1
  125. package/dist/store/sqlite/meeting-findings.js +1 -1
  126. package/dist/store/sqlite/meeting-stats.d.ts +2 -0
  127. package/dist/store/sqlite/meeting-stats.js +27 -1
  128. package/dist/store/sqlite/proposed-actions.d.ts +2 -0
  129. package/dist/store/sqlite/proposed-actions.js +24 -17
  130. package/dist/store/sqlite/reply-times.d.ts +57 -0
  131. package/dist/store/sqlite/reply-times.js +113 -0
  132. package/dist/store/sqlite/retention.js +2 -7
  133. package/dist/store/sqlite/task-board.js +2 -0
  134. package/dist/store/sqlite/task-stats.d.ts +32 -0
  135. package/dist/store/sqlite/task-stats.js +45 -0
  136. package/dist/store/sqlite/tasks.d.ts +2 -1
  137. package/dist/store/sqlite/tasks.js +2 -0
  138. package/dist/store/sqlite/work-tasks.d.ts +12 -2
  139. package/dist/store/sqlite/work-tasks.js +35 -26
  140. package/dist/store/store.d.ts +1 -0
  141. package/package.json +1 -1
  142. package/parity.json +37 -1
@@ -8,10 +8,12 @@ import { LOCAL_READ } from "../../cli/effects.js";
8
8
  import { listOf, renderAnswer } from "../../cli/envelope.js";
9
9
  import { listQuery, nativeField, nativeTerms, queryHelp, typedQuery } from "../../cli/list-query.js";
10
10
  import { pointsOf } from "../../cli/messenger/catch-up.js";
11
- import { catchUpQuery, summaryOf } from "../../cli/messenger/catch-up-answers.js";
11
+ import { catchUpQuery, SUMMARY_UNTRUSTED, summaryOf } from "../../cli/messenger/catch-up-answers.js";
12
12
  import { messageItem } from "../../cli/messenger/message-items.js";
13
13
  import { checkPoints } from "../../cli/messenger/points.js";
14
14
  import { unansweredHours } from "../../cli/messenger/review.js";
15
+ import { MAIL_HIT_UNTRUSTED } from "../../cli/messenger/search-answers.js";
16
+ import { mailOwnText } from "../../conversations/mail-text.js";
15
17
  import { parseDuration } from "../../domain/duration.js";
16
18
  import { formatReference } from "../../domain/references.js";
17
19
  import { AGENDA_DEFAULTS } from "../../services/agenda.js";
@@ -20,6 +22,7 @@ import { readPastMark } from "../marks.js";
20
22
  import { hitText } from "../read.js";
21
23
  import { OWED_DAYS, REVIEW_THREADS, reviewMail } from "../review.js";
22
24
  const DAY = 86_400_000;
25
+ const REVIEW_UNTRUSTED = [...SUMMARY_UNTRUSTED, ...MAIL_HIT_UNTRUSTED];
23
26
  /** Where a mail review starts: a time only; one thread is `conversations show`. */
24
27
  export const MAIL_REVIEW_QUERY = defineQuery({
25
28
  entity: "message",
@@ -29,31 +32,44 @@ export const MAIL_REVIEW_QUERY = defineQuery({
29
32
  nativeField({ name: "bulk", type: "kw", operators: ["term"], example: "bulk:false" }),
30
33
  ],
31
34
  });
32
- /** The mail review's steps; a host lists them beside its own so its step checks know them. */
33
- export const MAIL_REVIEW_STEPS = defineSteps([
34
- {
35
- id: "mail-messages.acknowledge",
36
- command: "messages acknowledge",
37
- args: [
38
- { name: "holder", required: true },
39
- { name: "cursor", required: true },
40
- ],
41
- why: "once these are processed, this moves the holder's mark past them",
42
- },
43
- {
44
- id: "mail-review.next",
45
- command: "review",
46
- args: [{ name: "query", required: true }],
47
- why: "the next review starts where this one ended",
48
- },
49
- {
50
- id: "mail-conversations.show",
51
- command: "conversations show",
52
- args: [{ name: "conversation", required: true }],
53
- why: "only the newest emails of the thread were read here; this reads it whole",
54
- },
55
- ]);
56
- const step = (id) => MAIL_REVIEW_STEPS.find((one) => one.id === id);
35
+ /** The mail review's steps where a host mounted them; a host lists them beside its own so its step checks know them. */
36
+ export const mailReviewSteps = (mount = {}) => {
37
+ const mailbox = mount.mailbox ? [{ name: mount.mailbox.option, required: true }] : [];
38
+ return defineSteps([
39
+ {
40
+ id: "mail-messages.acknowledge",
41
+ command: mount.acknowledge ?? "messages acknowledge",
42
+ args: [{ name: "holder", required: true }, { name: "cursor", required: true }, ...mailbox],
43
+ why: "once these are processed, this moves the holder's mark past them",
44
+ },
45
+ {
46
+ id: "mail-review.next",
47
+ command: mount.review ?? "review",
48
+ args: [{ name: "query", required: true }, ...mailbox],
49
+ why: "the next review starts where this one ended",
50
+ },
51
+ ...(mount.conversation === null
52
+ ? []
53
+ : [
54
+ {
55
+ id: "mail-conversations.show",
56
+ command: mount.conversation ?? "conversations show",
57
+ args: [{ name: "conversation", required: true }, ...mailbox],
58
+ why: "only the newest emails of the thread were read here; this reads it whole",
59
+ },
60
+ ]),
61
+ ]);
62
+ };
63
+ /** The steps of a mail CLI, whose profile names the mailbox. */
64
+ export const MAIL_REVIEW_STEPS = mailReviewSteps();
65
+ const stepsFor = (mount, mailbox) => {
66
+ const definitions = mailReviewSteps(mount);
67
+ const where = mount?.mailbox ? { [mount.mailbox.option]: mount.mailbox.value(mailbox) } : {};
68
+ return (id, args) => {
69
+ const definition = definitions.find((one) => one.id === id);
70
+ return definition ? [stepOf(definition, { args: { ...args, ...where } })] : [];
71
+ };
72
+ };
57
73
  const waitingHours = (value) => value === true ? AGENDA_DEFAULTS.waiting / 3_600_000 : parseDuration(String(value), "--waiting") / 3_600_000;
58
74
  const quoted = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
59
75
  /** A file name for the mailbox's points: they describe the mailbox, whichever profile reads it. */
@@ -67,20 +83,21 @@ const markText = (marks) => {
67
83
  const shown = said.filter(Boolean).join(" · ");
68
84
  return shown ? `\n [${shown}]` : "";
69
85
  };
70
- const reviewAnswer = (found, mailbox, fresh) => {
86
+ const reviewAnswer = (found, mailbox, fresh, step) => {
71
87
  const read = found.chats
72
88
  .flatMap((chat) => chat.messages.map((message) => ({ ...message, chatTitle: chat.title })))
73
89
  .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
74
90
  const cut = found.chats.filter((chat) => chat.more);
75
91
  const ref = (thread) => formatReference({ type: "chat", ...mailbox, chat: thread });
76
92
  const turns = found.unanswered !== undefined || found.waiting !== undefined;
93
+ const wholeThreads = cut.flatMap((chat) => step("mail-conversations.show", { conversation: ref(chat.id) }));
77
94
  const missing = [
78
95
  ...(cut.length > 0
79
96
  ? [
80
97
  {
81
98
  field: "items",
82
99
  why: `only the newest emails of ${cut.length} thread(s)`,
83
- step: "mail-conversations.show",
100
+ ...(wholeThreads.length ? { step: "mail-conversations.show" } : {}),
84
101
  },
85
102
  ]
86
103
  : []),
@@ -98,13 +115,9 @@ const reviewAnswer = (found, mailbox, fresh) => {
98
115
  ];
99
116
  const next = [
100
117
  ...(!fresh && !turns
101
- ? [
102
- stepOf(step("mail-review.next"), {
103
- args: { query: `date>${quoted(found.complete ? found.until : found.since)}` },
104
- }),
105
- ]
118
+ ? step("mail-review.next", { query: `date>${quoted(found.complete ? found.until : found.since)}` })
106
119
  : []),
107
- ...cut.map((chat) => stepOf(step("mail-conversations.show"), { args: { conversation: ref(chat.id) } })),
120
+ ...wholeThreads,
108
121
  ];
109
122
  return listOf("review", {
110
123
  items: read.map((message) => ({
@@ -122,6 +135,7 @@ const reviewAnswer = (found, mailbox, fresh) => {
122
135
  }
123
136
  : {}),
124
137
  },
138
+ untrusted: REVIEW_UNTRUSTED,
125
139
  })),
126
140
  limit: Math.max(1, read.length),
127
141
  has_more: false,
@@ -142,8 +156,10 @@ const holderReview = async (command, mail, holder, query) => {
142
156
  : new Map();
143
157
  return { read, mailbox, marks };
144
158
  });
159
+ // An agent reads this: earlier mail and signatures stay out of its text; the fingerprint is over the stored body.
160
+ const emails = read.emails.map((email) => ({ ...email, text: mailOwnText(email.text) }));
145
161
  const chats = new Map();
146
- for (const email of read.emails) {
162
+ for (const email of emails) {
147
163
  const chat = chats.get(email.chatId) ?? {
148
164
  id: email.chatId,
149
165
  title: email.chatTitle,
@@ -165,12 +181,10 @@ const holderReview = async (command, mail, holder, query) => {
165
181
  partial: false,
166
182
  quiet: 0,
167
183
  };
168
- const answer = reviewAnswer(found, mailbox, true);
184
+ const step = stepsFor(mail.mount, mailbox);
185
+ const answer = reviewAnswer(found, mailbox, true, step);
169
186
  const skipped = Object.entries(read.skipped).filter(([, count]) => count > 0);
170
- answer.next = [
171
- stepOf(step("mail-messages.acknowledge"), { args: { holder, cursor: read.cursor } }),
172
- ...(answer.next ?? []),
173
- ];
187
+ answer.next = [...step("mail-messages.acknowledge", { holder, cursor: read.cursor }), ...(answer.next ?? [])];
174
188
  answer.missing = [
175
189
  ...(answer.missing ?? []),
176
190
  ...(read.held > 0 ? [{ field: "items", why: `${read.held} partial emails held until they are whole` }] : []),
@@ -183,7 +197,7 @@ const holderReview = async (command, mail, holder, query) => {
183
197
  output.renderer.note(read.mark
184
198
  ? `${holder}: past ${read.mark.throughId}; acknowledge with --cursor ${read.cursor}${read.more ? "; more after these" : ""}`
185
199
  : `${holder} has no mark yet: it starts from now — acknowledge with --cursor ${read.cursor}`);
186
- return read.emails.map((hit) => `${hitText(hit)}${markText(marks.get(hit.id))}\n`).join("");
200
+ return emails.map((hit) => `${hitText(hit)}${markText(marks.get(hit.id))}\n`).join("");
187
201
  });
188
202
  };
189
203
  /**
@@ -209,7 +223,8 @@ export const mailReviewCommand = (mail) => annotate(new Command("review"), {
209
223
  .option("--unanswered [duration]", "only threads whose newest email came to you, in To or Cc, with no answer for this long — 4h, 1d; 24h if not given")
210
224
  .option("--waiting [duration]", "only threads where you wrote last and nobody answered for this long — 3d; 7d if not given")
211
225
  .option("--holder <name>", "read for this holder — an agent or a pipeline — what is stored past its processing mark, by store order; " +
212
- "bulk, envelopes, drafts, suspicious senders and partial mail left out; `messages acknowledge` moves the mark")
226
+ "bulk, envelopes, drafts, suspicious senders and partial mail left out; each text without quoted earlier mail " +
227
+ "or the signature; `messages acknowledge` moves the mark")
213
228
  .option("--new", "what changed since the last `review --new` of this mailbox, a point per thread — for scheduled runs")
214
229
  .addHelpText("after", `
215
230
  Without a date, --unanswered and --waiting look at threads active in the last ${OWED_DAYS} days.
@@ -254,7 +269,7 @@ an agent over MCP, and each scheduled run.`)
254
269
  return { found, mailbox, saved };
255
270
  });
256
271
  const output = outputFor(this);
257
- const answer = reviewAnswer(found, mailbox, saved !== undefined);
272
+ const answer = reviewAnswer(found, mailbox, saved !== undefined, stepsFor(mail.mount, mailbox));
258
273
  if (meta)
259
274
  answer.meta = { ...meta, ...answer.meta };
260
275
  renderAnswer(this, output, answer, () => {
@@ -1,4 +1,6 @@
1
+ export { mailOwnText } from "../conversations/mail-text.js";
1
2
  export { mailAccountKey } from "./account-key.js";
3
+ export { DEFAULT_ATTACHMENT_POLICY, type MailAttachmentPolicy, POLICY_SKIPS } from "./attachments.js";
2
4
  export { BULK, bulkBox } from "./bulk.js";
3
5
  export { factsOf } from "./facts.js";
4
6
  export { MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, type MailFinding, type MailFindingsInput, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
@@ -27,5 +29,7 @@ export { type MailCriteria, mailCriteriaOf, narrows } from "./server-query.js";
27
29
  export { type MailServerAccess, type MailServerRequest, searchMailServer } from "./server-search.js";
28
30
  export type { Listed, MailSource, Quick, Searched } from "./source.js";
29
31
  export { SEARCH_REFUSED, searchRefused } from "./source.js";
32
+ export { MAIL_STATS_BY, type MailStats, type MailStatsBy, type MailStatsRequest, type MailStatsRow, mailStats, mailStatsItem, } from "./stats.js";
30
33
  export { listedOf, type StoredMail } from "./stored.js";
31
34
  export { MAX_ATTACHMENT_CHARS, type MailAccount, type MailIndexed, type MailSyncOptions, type MailSyncResult, RETRYABLE_ATTACHMENTS, syncMail, } from "./sync.js";
35
+ export { readEmailWhole, type WholeAttachment } from "./whole.js";
@@ -1,4 +1,6 @@
1
+ export { mailOwnText } from "../conversations/mail-text.js";
1
2
  export { mailAccountKey } from "./account-key.js";
3
+ export { DEFAULT_ATTACHMENT_POLICY, POLICY_SKIPS } from "./attachments.js";
2
4
  export { BULK, bulkBox } from "./bulk.js";
3
5
  export { factsOf } from "./facts.js";
4
6
  export { MAIL_FINDINGS_KIND, MAIL_FINDINGS_PER_RUN, MAIL_FINDINGS_PER_SENDER, mailFindingFingerprint, validateMailFindings, } from "./findings.js";
@@ -26,5 +28,7 @@ export { DEMOTE_AT, demotedSenders, SCORE_VERSION, scoreSenders, } from "./sende
26
28
  export { mailCriteriaOf, narrows } from "./server-query.js";
27
29
  export { searchMailServer } from "./server-search.js";
28
30
  export { SEARCH_REFUSED, searchRefused } from "./source.js";
31
+ export { MAIL_STATS_BY, mailStats, mailStatsItem, } from "./stats.js";
29
32
  export { listedOf } from "./stored.js";
30
33
  export { MAX_ATTACHMENT_CHARS, RETRYABLE_ATTACHMENTS, syncMail, } from "./sync.js";
34
+ export { readEmailWhole } from "./whole.js";
@@ -1,6 +1,7 @@
1
1
  import type { BudgetStop } from "../background/budget.js";
2
2
  import type { LoadEngine } from "../readers/types.js";
3
3
  import type { MessageStore } from "../store/index.js";
4
+ import { type MailAttachmentPolicy } from "./attachments.js";
4
5
  import type { MailSource } from "./source.js";
5
6
  /** The `emails.metadata` key of an email stored from its first bytes: `{ bytes }` read of its `size`. */
6
7
  export declare const PARTIAL = "partial";
@@ -23,7 +24,7 @@ export interface PartialOutcome {
23
24
  * `identify` gives the stored id for the bytes read there. Each failed read is counted, and after `PARTIAL_TRIES`, or
24
25
  * at once when the place holds another message, the email is left partial for good.
25
26
  */
26
- export declare const completePartial: ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, loadEngine, now, take, signal, }: {
27
+ export declare const completePartial: ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, policy, loadEngine, now, take, signal, }: {
27
28
  store: MessageStore;
28
29
  source: MailSource;
29
30
  accountId: number;
@@ -32,6 +33,7 @@ export declare const completePartial: ({ store, source, accountId, address, fold
32
33
  folders: () => Promise<readonly string[]>;
33
34
  maxBodyChars: number;
34
35
  maxAttachmentChars: number;
36
+ policy?: MailAttachmentPolicy;
35
37
  loadEngine: LoadEngine;
36
38
  now: number;
37
39
  /** A budgeted run's next request, or why there is none. */
@@ -15,7 +15,7 @@ export const PARTIAL_TRIES = 3;
15
15
  * `identify` gives the stored id for the bytes read there. Each failed read is counted, and after `PARTIAL_TRIES`, or
16
16
  * at once when the place holds another message, the email is left partial for good.
17
17
  */
18
- export const completePartial = async ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, loadEngine, now, take, signal, }) => {
18
+ export const completePartial = async ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, policy, loadEngine, now, take, signal, }) => {
19
19
  const { emails, total, stuck } = await store.mail.partialEmails(accountId, PARTIAL_PER_RUN);
20
20
  if (emails.length === 0)
21
21
  return { completed: 0, left: 0, stuck };
@@ -89,7 +89,7 @@ export const completePartial = async ({ store, source, accountId, address, folde
89
89
  }
90
90
  try {
91
91
  const mail = await parseMail(raw, maxBodyChars);
92
- const files = await readAttachments(mail, email.externalId, { loadEngine, maxAttachmentChars, at: now, ...(signal ? { signal } : {}) }, attachments);
92
+ const files = await readAttachments(mail, email.externalId, { loadEngine, maxAttachmentChars, at: now, ...(policy ? { policy } : {}), ...(signal ? { signal } : {}) }, attachments);
93
93
  const { [PARTIAL]: _partial, ...metadata } = email.metadata ?? {};
94
94
  const whole = { ...item, messageId: email.externalId, threadId: email.threadExternalId };
95
95
  await store.mail.saveThread({
@@ -1,5 +1,6 @@
1
1
  import type { LoadEngine } from "../readers/types.js";
2
2
  import type { MessageStore } from "../store/index.js";
3
+ import { type MailAttachmentPolicy } from "./attachments.js";
3
4
  import type { MailSource } from "./source.js";
4
5
  import type { MailAccount, MailSyncOptions, MailSyncResult } from "./sync.js";
5
6
  /** Failures a newer install mends: the reader was missing, not the file at fault, so reading again can succeed. */
@@ -9,7 +10,7 @@ export declare const RETRYABLE_ATTACHMENTS: string[];
9
10
  * attachment file, so the email comes from the source again. Nothing is listed, so no deletion is proved and no
10
11
  * resume point moves; the email keeps its stored mailboxes, flags and rule.
11
12
  */
12
- export declare const retryUnread: ({ store, source, account, since, max, maxBodyChars, maxAttachmentChars, now, loadEngine, index, }: {
13
+ export declare const retryUnread: ({ store, source, account, since, max, maxBodyChars, maxAttachmentChars, policy, now, loadEngine, index, }: {
13
14
  store: MessageStore;
14
15
  source: MailSource;
15
16
  account: MailAccount;
@@ -17,6 +18,7 @@ export declare const retryUnread: ({ store, source, account, since, max, maxBody
17
18
  max: number;
18
19
  maxBodyChars: number;
19
20
  maxAttachmentChars: number;
21
+ policy?: MailAttachmentPolicy;
20
22
  now: () => number;
21
23
  loadEngine: LoadEngine;
22
24
  index: MailSyncOptions["index"];
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { mailAccountKey } from "./account-key.js";
3
- import { addressOf, FETCH_CHUNK, readAttachments } from "./attachments.js";
3
+ import { addressOf, FETCH_CHUNK, readAttachments, } from "./attachments.js";
4
4
  import { sentAtOf } from "./email-input.js";
5
5
  import { mailFolders } from "./folders.js";
6
6
  import { mailHeadersOf } from "./headers.js";
@@ -12,7 +12,7 @@ export const RETRYABLE_ATTACHMENTS = ["engine-missing"];
12
12
  * attachment file, so the email comes from the source again. Nothing is listed, so no deletion is proved and no
13
13
  * resume point moves; the email keeps its stored mailboxes, flags and rule.
14
14
  */
15
- export const retryUnread = async ({ store, source, account, since, max, maxBodyChars, maxAttachmentChars, now, loadEngine, index, }) => {
15
+ export const retryUnread = async ({ store, source, account, since, max, maxBodyChars, maxAttachmentChars, policy, now, loadEngine, index, }) => {
16
16
  const startedAt = now();
17
17
  const key = mailAccountKey(account.address);
18
18
  const find = source.find;
@@ -55,7 +55,7 @@ export const retryUnread = async ({ store, source, account, since, max, maxBodyC
55
55
  continue;
56
56
  }
57
57
  const before = new Map(stored.attachments.map(({ position, extractionError }) => [position, extractionError]));
58
- const files = await readAttachments(mail, item.messageId, { loadEngine, maxAttachmentChars, at: startedAt }, attachments);
58
+ const files = await readAttachments(mail, item.messageId, { loadEngine, maxAttachmentChars, at: startedAt, ...(policy ? { policy } : {}) }, attachments);
59
59
  for (const file of files)
60
60
  if (RETRYABLE_ATTACHMENTS.includes(before.get(file.position) ?? ""))
61
61
  if (file.extraction === "text")
@@ -0,0 +1,50 @@
1
+ import type { Saved } from "../services/message-resolution.js";
2
+ import type { AccountKey, MessageStore } from "../store/store.js";
3
+ export declare const MAIL_STATS_BY: readonly ["day", "week", "sender"];
4
+ export type MailStatsBy = (typeof MAIL_STATS_BY)[number];
5
+ export interface MailStatsRequest {
6
+ by: MailStatsBy;
7
+ /** The query of `search mail`; every stored email when unset. */
8
+ text?: string;
9
+ /** Only these mailboxes; every stored one when neither this nor `mailAccount` is given. */
10
+ accounts?: AccountKey[];
11
+ /** Only this mailbox, by its address. */
12
+ mailAccount?: string;
13
+ exact?: boolean;
14
+ /** Where calendar days and weeks start; the system timezone when unset, as for search. */
15
+ timezone?: string;
16
+ /** Rows kept: the busiest senders, or the latest days and weeks, oldest first. */
17
+ limit: number;
18
+ }
19
+ export interface MailStatsRow {
20
+ /** `YYYY-MM-DD` (a week by its Monday), or the sender's address. */
21
+ key: string;
22
+ name: string | null;
23
+ received: number;
24
+ sent: number;
25
+ }
26
+ export interface MailStats {
27
+ by: MailStatsBy;
28
+ received: number;
29
+ sent: number;
30
+ rows: MailStatsRow[];
31
+ hasMore: boolean;
32
+ timezone: string;
33
+ }
34
+ /**
35
+ * Stored mail the `search mail` query matches, received and sent, per calendar day, week or sender; counts only.
36
+ * `--by sender` counts received mail: the sender of sent mail is the owner. Reads the store, records no search.
37
+ */
38
+ export declare const mailStats: (store: MessageStore, runAs: AccountKey, { by, text, accounts, mailAccount, exact, timezone, limit }: MailStatsRequest, saved?: Saved) => Promise<MailStats>;
39
+ /** The `mail_counts` entity: what a command answers as its `item`. */
40
+ export declare const mailStatsItem: ({ by, received, sent, rows }: MailStats) => {
41
+ by: "day" | "sender" | "week";
42
+ received: number;
43
+ sent: number;
44
+ rows: {
45
+ key: string;
46
+ name: string | null;
47
+ received: number;
48
+ sent: number;
49
+ }[];
50
+ };
@@ -0,0 +1,60 @@
1
+ import { calendarKey, weekKey } from "../domain/calendar.js";
2
+ import { prepareLucene } from "../services/messages-search.js";
3
+ export const MAIL_STATS_BY = ["day", "week", "sender"];
4
+ /**
5
+ * Stored mail the `search mail` query matches, received and sent, per calendar day, week or sender; counts only.
6
+ * `--by sender` counts received mail: the sender of sent mail is the owner. Reads the store, records no search.
7
+ */
8
+ export const mailStats = async (store, runAs, { by, text, accounts, mailAccount, exact, timezone, limit }, saved = {}) => {
9
+ const prepared = await prepareLucene(store, runAs, {
10
+ kind: "mail",
11
+ limit: 1,
12
+ ...(text?.trim() ? { text } : {}),
13
+ ...(accounts === undefined ? {} : { accounts }),
14
+ ...(mailAccount === undefined ? {} : { mailAccount }),
15
+ ...(exact ? { exact: true } : {}),
16
+ ...(timezone === undefined ? {} : { timezone }),
17
+ }, saved);
18
+ const mailboxes = prepared.scopeAccounts.filter(({ provider }) => provider === "email");
19
+ const groups = mailboxes.length
20
+ ? await store.queries.countQuery({ ...prepared.execution, accounts: mailboxes, corpus: "mail" }, by === "sender" ? "sender_direction" : "time_direction")
21
+ : [];
22
+ const received = groups.filter(({ outgoing }) => !outgoing).reduce((sum, { count }) => sum + count, 0);
23
+ const sent = groups.filter(({ outgoing }) => outgoing).reduce((sum, { count }) => sum + count, 0);
24
+ let rows;
25
+ if (by === "sender")
26
+ rows = groups
27
+ .filter(({ outgoing }) => !outgoing)
28
+ .map(({ id, name, count }) => ({ key: id ?? "unknown", name, received: count, sent: 0 }))
29
+ .sort((a, b) => b.received - a.received || a.key.localeCompare(b.key));
30
+ else {
31
+ const day = calendarKey(prepared.timezone, "day");
32
+ const keyOf = by === "week" ? (at) => weekKey(day(at)) : day;
33
+ const totals = new Map();
34
+ for (const { id, outgoing, count } of groups) {
35
+ const key = keyOf(Number(id));
36
+ const row = totals.get(key) ?? { key, name: null, received: 0, sent: 0 };
37
+ if (outgoing)
38
+ row.sent += count;
39
+ else
40
+ row.received += count;
41
+ totals.set(key, row);
42
+ }
43
+ rows = [...totals.values()].sort((a, b) => a.key.localeCompare(b.key));
44
+ }
45
+ return {
46
+ by,
47
+ received,
48
+ sent,
49
+ rows: by === "sender" ? rows.slice(0, limit) : rows.slice(-limit),
50
+ hasMore: rows.length > limit,
51
+ timezone: prepared.timezone,
52
+ };
53
+ };
54
+ /** The `mail_counts` entity: what a command answers as its `item`. */
55
+ export const mailStatsItem = ({ by, received, sent, rows }) => ({
56
+ by,
57
+ received,
58
+ sent,
59
+ rows: rows.map(({ key, name, received, sent }) => ({ key, name, received, sent })),
60
+ });
@@ -1,6 +1,7 @@
1
1
  import type { BudgetStop, RunBudget } from "../background/budget.js";
2
2
  import type { LoadEngine } from "../readers/types.js";
3
3
  import type { AccountKey, MessageStore } from "../store/index.js";
4
+ import type { MailAttachmentPolicy } from "./attachments.js";
4
5
  import type { PartialOutcome } from "./partial.js";
5
6
  import type { TagOutcome } from "./rule-tags.js";
6
7
  import type { MailRule } from "./rules.js";
@@ -84,6 +85,8 @@ export interface MailSyncOptions {
84
85
  max?: number;
85
86
  maxBodyChars?: number;
86
87
  maxAttachmentChars?: number;
88
+ /** Whether attachments are read into the index, and up to what size; `DEFAULT_ATTACHMENT_POLICY` when left out. */
89
+ attachments?: MailAttachmentPolicy;
87
90
  now?: () => number;
88
91
  /** Domain registration dates already known, for the sender score a rule asks for. */
89
92
  ages?: ReadonlyMap<string, DomainAge>;
package/dist/mail/sync.js CHANGED
@@ -3,7 +3,7 @@ import { CliError } from "@wirecat/cli-base";
3
3
  import { importEngine } from "../attachments/extract.js";
4
4
  import { isBusy } from "../store/sqlite/transaction.js";
5
5
  import { mailAccountKey } from "./account-key.js";
6
- import { FETCH_CHUNK, MAX_ATTACHMENT_CHARS, readAttachments } from "./attachments.js";
6
+ import { DEFAULT_ATTACHMENT_POLICY, FETCH_CHUNK, MAX_ATTACHMENT_CHARS, readAttachments } from "./attachments.js";
7
7
  import { BULK, bulkBox } from "./bulk.js";
8
8
  import { parsedEmail } from "./email-input.js";
9
9
  import { factsOf } from "./facts.js";
@@ -51,6 +51,7 @@ export const syncMail = async (source, store, options) => {
51
51
  folders: () => mailFolders(options.account, source),
52
52
  maxBodyChars: options.maxBodyChars ?? MAX_BODY_CHARS,
53
53
  maxAttachmentChars: options.maxAttachmentChars ?? MAX_ATTACHMENT_CHARS,
54
+ policy: options.attachments ?? DEFAULT_ATTACHMENT_POLICY,
54
55
  loadEngine: options.loadEngine ?? importEngine,
55
56
  now: (options.now ?? Date.now)(),
56
57
  ...(options.budget ? { take: () => options.budget?.take() ?? null } : {}),
@@ -169,7 +170,7 @@ const syncRounds = async (source, store, options) => {
169
170
  : {}),
170
171
  };
171
172
  };
172
- const syncRound = async (source, store, { account, since, max = 200, maxBodyChars = MAX_BODY_CHARS, maxAttachmentChars = MAX_ATTACHMENT_CHARS, now = Date.now, ages, loadEngine = importEngine, retryAttachments = false, rescan = false, saveCoverage = true, resumeFrom = RESUME_FROM, index, signal, }, plan) => {
173
+ const syncRound = async (source, store, { account, since, max = 200, maxBodyChars = MAX_BODY_CHARS, maxAttachmentChars = MAX_ATTACHMENT_CHARS, attachments: policy = DEFAULT_ATTACHMENT_POLICY, now = Date.now, ages, loadEngine = importEngine, retryAttachments = false, rescan = false, saveCoverage = true, resumeFrom = RESUME_FROM, index, signal, }, plan) => {
173
174
  if (retryAttachments)
174
175
  return retryUnread({
175
176
  store,
@@ -179,6 +180,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
179
180
  max,
180
181
  maxBodyChars,
181
182
  maxAttachmentChars,
183
+ policy,
182
184
  now,
183
185
  loadEngine,
184
186
  index,
@@ -759,7 +761,7 @@ const syncRound = async (source, store, { account, since, max = 200, maxBodyChar
759
761
  plan?.unreadable.delete(item.messageId);
760
762
  const header = mailHeadersOf(raws[index]);
761
763
  // A stop between attachments ends the chunk; what the earlier slices saved stays.
762
- const files = await readAttachments(mail, item.messageId, { loadEngine, maxAttachmentChars, at: startedAt, ...(signal ? { signal } : {}) }, attachments).catch(async (error) => {
764
+ const files = await readAttachments(mail, item.messageId, { loadEngine, maxAttachmentChars, at: startedAt, policy, ...(signal ? { signal } : {}) }, attachments).catch(async (error) => {
763
765
  await stopIfAborted();
764
766
  throw error;
765
767
  });
@@ -0,0 +1,28 @@
1
+ import type { LoadEngine } from "../readers/types.js";
2
+ import type { MessageStore } from "../store/index.js";
3
+ import type { MailSource } from "./source.js";
4
+ export interface WholeAttachment {
5
+ position: number;
6
+ name: string;
7
+ mime: string | null;
8
+ /** `null` when the email held no content for it. */
9
+ bytes: Uint8Array | null;
10
+ status: string;
11
+ }
12
+ /**
13
+ * Reads one stored email from its source again and every attachment of it, whatever its size: what a person asks for
14
+ * by name is read even when a sync's policy left it. The new text is saved as a sync saves it, so the search index
15
+ * takes it; the files are only answered, never written. Gmail's email is found by its id in `folders`; elsewhere it is
16
+ * read at its newest stored place, trusted only when `identify` gives the stored id for the bytes read there.
17
+ */
18
+ export declare const readEmailWhole: ({ store, source, address, externalId, folders, maxBodyChars, maxAttachmentChars, loadEngine, now, }: {
19
+ store: MessageStore;
20
+ source: MailSource;
21
+ address: string;
22
+ externalId: string;
23
+ folders: () => Promise<readonly string[]>;
24
+ maxBodyChars?: number;
25
+ maxAttachmentChars?: number;
26
+ loadEngine?: LoadEngine;
27
+ now?: number;
28
+ }) => Promise<WholeAttachment[]>;
@@ -0,0 +1,70 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { importEngine } from "../attachments/extract.js";
3
+ import { mailAccountKey } from "./account-key.js";
4
+ import { MAX_ATTACHMENT_CHARS, readAttachments } from "./attachments.js";
5
+ import { parsedEmail } from "./email-input.js";
6
+ import { mailHeadersOf } from "./headers.js";
7
+ import { MAX_BODY_CHARS, parseMail } from "./parse.js";
8
+ import { PARTIAL } from "./partial.js";
9
+ /**
10
+ * Reads one stored email from its source again and every attachment of it, whatever its size: what a person asks for
11
+ * by name is read even when a sync's policy left it. The new text is saved as a sync saves it, so the search index
12
+ * takes it; the files are only answered, never written. Gmail's email is found by its id in `folders`; elsewhere it is
13
+ * read at its newest stored place, trusted only when `identify` gives the stored id for the bytes read there.
14
+ */
15
+ export const readEmailWhole = async ({ store, source, address, externalId, folders, maxBodyChars = MAX_BODY_CHARS, maxAttachmentChars = MAX_ATTACHMENT_CHARS, loadEngine = importEngine, now = Date.now(), }) => {
16
+ const { id: accountId } = await store.accounts.storedAccount(mailAccountKey(address));
17
+ const stored = await store.mail.emailPlace(accountId, externalId);
18
+ if (stored === null)
19
+ throw new CliError("not_found", `${externalId} is not stored for ${address}`);
20
+ let item;
21
+ if (source.find)
22
+ for (const folder of await folders()) {
23
+ item = (await source.find([externalId], folder)).find(({ messageId }) => messageId === externalId);
24
+ if (item)
25
+ break;
26
+ }
27
+ else {
28
+ const place = stored.locations[0];
29
+ if (place)
30
+ item = {
31
+ uid: String(place.uid),
32
+ folder: place.folder,
33
+ validity: place.uidvalidity,
34
+ receivedAt: new Date(stored.receivedAt ?? 0).toISOString(),
35
+ };
36
+ }
37
+ if (item === undefined)
38
+ throw new CliError("not_found", `the mail server no longer gives ${externalId}`);
39
+ const [raw] = await source.fetchRaw([item]);
40
+ if (!(raw instanceof Uint8Array))
41
+ throw new CliError("provider_unavailable", `${externalId} could not be read from the mail server`);
42
+ if (!source.find && source.identify?.(raw, item).messageId !== externalId)
43
+ throw new CliError("not_found", `the mail server holds another email where ${externalId} was`);
44
+ const mail = await parseMail(raw, maxBodyChars);
45
+ const log = [];
46
+ const files = await readAttachments(mail, externalId, { loadEngine, maxAttachmentChars, at: now }, log);
47
+ const { [PARTIAL]: _partial, ...metadata } = stored.metadata ?? {};
48
+ const whole = { ...item, messageId: externalId, threadId: stored.threadExternalId };
49
+ await store.mail.saveThread({
50
+ accountId,
51
+ externalId: stored.threadExternalId,
52
+ now,
53
+ emails: [
54
+ {
55
+ ...parsedEmail(mail, whole, mailHeadersOf(raw), address),
56
+ receivedAt: stored.receivedAt,
57
+ size: raw.byteLength,
58
+ metadata,
59
+ attachments: files,
60
+ },
61
+ ],
62
+ });
63
+ return mail.attachments.map((part, position) => ({
64
+ position,
65
+ name: part.name,
66
+ mime: part.mime,
67
+ bytes: part.bytes,
68
+ status: log[position]?.status ?? "unavailable",
69
+ }));
70
+ };
@@ -32,7 +32,7 @@ export { type MeetingProjectEvidenceAnswer, type MeetingProjectEvidenceItem, mee
32
32
  export { MEETING_PROJECT_EVIDENCE_QUERY, type MeetingProjectEvidenceRequest, prepareMeetingProjectEvidence, } from "./project-evidence-query.js";
33
33
  export { type MeetingProviderScope, snapshotMeetingScope } from "./provider.js";
34
34
  export { type PullOptions, pull, type RunReport } from "./pull.js";
35
- export { event, eventItem, meetingLink, meetingLinkItem, meetingParticipants, meetingPassage, meetingReview, meetingReviewItem, meetingSpeaker, meetingSpeakerItem, meetingStats, meetingStatsItem, } from "./read-answers.js";
35
+ export { event, eventItem, meetingCounts, meetingCountsItem, meetingLink, meetingLinkItem, meetingParticipants, meetingPassage, meetingReview, meetingReviewItem, meetingSpeaker, meetingSpeakerItem, meetingStats, meetingStatsItem, } from "./read-answers.js";
36
36
  export type { MeetingContextReadCapabilities, MeetingReadBudget, MeetingReadCapabilities, MeetingReadScope, TranscriptContextInput, TranscriptContextPage, TranscriptPage, TranscriptReadScope, TranscriptRowPage, } from "./read-store.js";
37
37
  export { listed, MeetingError, meetingDetailsAnswer, meetingPeople, meetingSummaryAnswer, meetingTranscriptAnswer, type TranscriptReadOptions, } from "./reads.js";
38
38
  export { installRecordingArchiveSchedule, parseRecordingArchiveSchedule, planRecordingArchiveSchedule, type RecordingArchiveSchedule, type RecordingArchiveScheduleInput, type RecordingArchiveScheduler, recordingArchiveScheduleWindow, } from "./recording-schedule.js";
@@ -51,7 +51,7 @@ export { createMeetingSeriesInventoryReader } from "./series-inventory.js";
51
51
  export type { MeetingSeriesCandidate, MeetingSeriesInventoryPage, MeetingSeriesInventoryPort, MeetingSeriesInventoryRead, MeetingSeriesInventoryRequest, MeetingSeriesInventoryResult, } from "./series-inventory-types.js";
52
52
  export { validateMeetingSeriesInventoryCursor } from "./series-inventory-validation.js";
53
53
  export type { MeetingSource, SourceParticipant } from "./source.js";
54
- export { aggregateMeetingStats, type MeetingSpeakerStats, type MeetingStats, type MeetingStatsInput, type MeetingStatsRow, type MeetingStatsSelection, type MeetingStatsStore, type MeetingWeekStats, } from "./stats.js";
54
+ export { aggregateMeetingCounts, aggregateMeetingStats, MEETING_COUNTS_GROUPINGS, type MeetingCounts, type MeetingCountsGrouping, type MeetingCountsInput, type MeetingCountsMeeting, type MeetingCountsRow, type MeetingSpeakerStats, type MeetingStats, type MeetingStatsInput, type MeetingStatsRow, type MeetingStatsSelection, type MeetingStatsStore, type MeetingWeekStats, NO_SERIES, } from "./stats.js";
55
55
  export type { ChatInput, ContractCase, EventCandidateFilter, IdentityInput, MeetingDetails, MeetingFilter, MeetingHitSource, MeetingInput, MeetingSave, MeetingStore, NewRecord, ParticipantInput, SearchHit, TranscriptInput, TranscriptLanguageFill, TranscriptRowInput, } from "./store.js";
56
56
  export { meetingTimestamp } from "./timestamp.js";
57
57
  export type { MeetingTranscriptReceiptStore, MeetingTranscriptStore, TranscriptAppend, TranscriptAppendReceipt, UnlinkedTranscriptInput, } from "./transcript-store.js";
@@ -24,7 +24,7 @@ export { meetingProjectEvidence, meetingProjectEvidenceAnswer, } from "./project
24
24
  export { MEETING_PROJECT_EVIDENCE_QUERY, prepareMeetingProjectEvidence, } from "./project-evidence-query.js";
25
25
  export { snapshotMeetingScope } from "./provider.js";
26
26
  export { pull } from "./pull.js";
27
- export { event, eventItem, meetingLink, meetingLinkItem, meetingParticipants, meetingPassage, meetingReview, meetingReviewItem, meetingSpeaker, meetingSpeakerItem, meetingStats, meetingStatsItem, } from "./read-answers.js";
27
+ export { event, eventItem, meetingCounts, meetingCountsItem, meetingLink, meetingLinkItem, meetingParticipants, meetingPassage, meetingReview, meetingReviewItem, meetingSpeaker, meetingSpeakerItem, meetingStats, meetingStatsItem, } from "./read-answers.js";
28
28
  export { listed, MeetingError, meetingDetailsAnswer, meetingPeople, meetingSummaryAnswer, meetingTranscriptAnswer, } from "./reads.js";
29
29
  export { installRecordingArchiveSchedule, parseRecordingArchiveSchedule, planRecordingArchiveSchedule, recordingArchiveScheduleWindow, } from "./recording-schedule.js";
30
30
  export { parseRecordingSyncCheckpoint, RecordingSyncError, recordingAssetFingerprint, recordingWindows, syncRecordingArchive, } from "./recording-sync.js";
@@ -38,7 +38,7 @@ export { validateMeetingHitSearch } from "./search-validation.js";
38
38
  export { validateMeetingSeriesHistory, } from "./series-history.js";
39
39
  export { createMeetingSeriesInventoryReader } from "./series-inventory.js";
40
40
  export { validateMeetingSeriesInventoryCursor } from "./series-inventory-validation.js";
41
- export { aggregateMeetingStats, } from "./stats.js";
41
+ export { aggregateMeetingCounts, aggregateMeetingStats, MEETING_COUNTS_GROUPINGS, NO_SERIES, } from "./stats.js";
42
42
  export { meetingTimestamp } from "./timestamp.js";
43
43
  export { parseTranscriptText, TranscriptTextError } from "./transcript-text.js";
44
44
  export { transcribeMeeting, } from "./transcription.js";