@wirecat/cli-core 0.561.0 → 0.563.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 (74) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/cli/app-doctor-command.js +3 -1
  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/index.d.ts +2 -1
  7. package/dist/cli/index.js +2 -1
  8. package/dist/cli/messenger/reply-stats-command.d.ts +4 -0
  9. package/dist/cli/messenger/reply-stats-command.js +57 -0
  10. package/dist/cli/messenger/search-command.js +8 -0
  11. package/dist/cli/messenger/stats-answers.d.ts +3 -0
  12. package/dist/cli/messenger/stats-answers.js +18 -0
  13. package/dist/cli/messenger/stats-command.js +5 -0
  14. package/dist/cli/output-budget.d.ts +26 -0
  15. package/dist/cli/output-budget.js +73 -0
  16. package/dist/cli/program.js +14 -1
  17. package/dist/cli/setting-defaults.d.ts +27 -0
  18. package/dist/cli/setting-defaults.js +36 -0
  19. package/dist/cli/settings.d.ts +5 -0
  20. package/dist/cli/settings.js +88 -5
  21. package/dist/cli/shared-command-exports.d.ts +8 -0
  22. package/dist/domain/admin-statistics.d.ts +5 -0
  23. package/dist/domain/admin-statistics.js +4 -3
  24. package/dist/entities/index.d.ts +31 -1
  25. package/dist/entities/index.js +4 -1
  26. package/dist/entities/reply-stats.d.ts +33 -0
  27. package/dist/entities/reply-stats.js +19 -0
  28. package/dist/errors/reasons.d.ts +1 -1
  29. package/dist/errors/reasons.js +2 -3
  30. package/dist/mail/attachments.d.ts +15 -2
  31. package/dist/mail/attachments.js +19 -3
  32. package/dist/mail/cli/index.d.ts +17 -1
  33. package/dist/mail/cli/index.js +1 -1
  34. package/dist/mail/cli/review.d.ts +5 -2
  35. package/dist/mail/cli/review.js +47 -39
  36. package/dist/mail/index.d.ts +2 -0
  37. package/dist/mail/index.js +2 -0
  38. package/dist/mail/partial.d.ts +3 -1
  39. package/dist/mail/partial.js +2 -2
  40. package/dist/mail/retry.d.ts +3 -1
  41. package/dist/mail/retry.js +3 -3
  42. package/dist/mail/sync-types.d.ts +3 -0
  43. package/dist/mail/sync.js +5 -3
  44. package/dist/mail/whole.d.ts +28 -0
  45. package/dist/mail/whole.js +69 -0
  46. package/dist/services/index.d.ts +2 -0
  47. package/dist/services/index.js +2 -0
  48. package/dist/services/proposal-tasks.js +3 -12
  49. package/dist/services/proposals/kinds/action.js +3 -2
  50. package/dist/services/proposals/registry.js +8 -4
  51. package/dist/services/proposals/types.d.ts +2 -0
  52. package/dist/services/reply-stats.d.ts +48 -0
  53. package/dist/services/reply-stats.js +137 -0
  54. package/dist/store/decisions.d.ts +14 -11
  55. package/dist/store/decisions.js +9 -2
  56. package/dist/store/index.d.ts +3 -3
  57. package/dist/store/index.js +2 -2
  58. package/dist/store/queries.d.ts +4 -0
  59. package/dist/store/queries.js +3 -0
  60. package/dist/store/sqlite/email-locations.d.ts +4 -0
  61. package/dist/store/sqlite/email-locations.js +26 -11
  62. package/dist/store/sqlite/lucene-fields.d.ts +2 -0
  63. package/dist/store/sqlite/lucene-fields.js +3 -1
  64. package/dist/store/sqlite/mail-findings.js +1 -1
  65. package/dist/store/sqlite/meeting-findings.js +1 -1
  66. package/dist/store/sqlite/proposed-actions.d.ts +2 -0
  67. package/dist/store/sqlite/proposed-actions.js +24 -17
  68. package/dist/store/sqlite/reply-times.d.ts +57 -0
  69. package/dist/store/sqlite/reply-times.js +113 -0
  70. package/dist/store/sqlite/work-tasks.d.ts +12 -2
  71. package/dist/store/sqlite/work-tasks.js +35 -26
  72. package/dist/store/store.d.ts +1 -0
  73. package/package.json +1 -1
  74. package/parity.json +24 -0
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { CliError, configFilePath, resolvePaths, saveConfigFile } from "@wirecat/cli-base";
3
3
  import * as v from "valibot";
4
- import { AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
4
+ import { AI_DEFAULTS, AI_ENTRIES, resolveAISettings } from "../analysis/settings.js";
5
5
  import { parseDuration } from "../domain/duration.js";
6
6
  import { DEFAULT_RERANK_MODEL, RERANK_MODELS } from "../embeddings/models.js";
7
7
  import { envName, readOnlyLockOf } from "../ports/app.js";
@@ -14,6 +14,7 @@ import { checkConfigText, issueMessage, readConfigFile, readConfigText, savedPer
14
14
  import { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
15
15
  import { profilesHere } from "./messenger/accounts.js";
16
16
  import { usableProfileName } from "./profile.js";
17
+ import { withSettingDefaults } from "./setting-defaults.js";
17
18
  import { permissionCeiling } from "./setup-answers.js";
18
19
  const DEFAULT_LIMIT = DEFAULT_CONFIG.defaults.limit;
19
20
  const DEFAULT_KEEP_RUNS_FOR_DAYS = DEFAULT_CONFIG.defaults.keep_runs_for_days;
@@ -107,6 +108,70 @@ const SHARED_DEFAULTS_ONLY = {
107
108
  update_check: v.optional(flag),
108
109
  skill_hint: v.optional(flag),
109
110
  };
111
+ /**
112
+ * Written into the file's `defaults` at the value the code applies when the file says nothing (WC-CMD-30). Left out:
113
+ * objects, and settings whose absence means something no value says (`timeout_ms`, `color`, `requests_per_minute`,
114
+ * the embedding model), since writing any value would change what they do.
115
+ */
116
+ const SHARED_SETTINGS = [
117
+ { key: "limit", default: DEFAULT_LIMIT, description: "how many items a list shows at once" },
118
+ { key: "keep_runs_for_days", default: DEFAULT_KEEP_RUNS_FOR_DAYS, description: "days a run is kept" },
119
+ {
120
+ key: "sends_per_hour",
121
+ default: DEFAULT_SENDS_PER_HOUR,
122
+ description: "the most messages an account sends in an hour; a bot has none unless its section sets one",
123
+ },
124
+ { key: "update_check", default: true, description: "check once a day for a newer version" },
125
+ {
126
+ key: "skill_hint",
127
+ default: true,
128
+ description: "tell an agent once a day that `skill install` would give it this tool's guide",
129
+ },
130
+ { key: "record", default: true, description: "keep every run in runs list; false keeps failed runs only" },
131
+ {
132
+ key: "record_input",
133
+ default: true,
134
+ description: 'keep what was typed in a run\'s command line: true, false, or "failed" for failed runs only',
135
+ },
136
+ {
137
+ key: "keep_successful_runs_for_days",
138
+ default: KEEP_SUCCESSFUL_RUNS_FOR_DAYS,
139
+ description: "days a successful run is kept",
140
+ },
141
+ { key: "runs_size_limit_mb", default: RUNS_SIZE_LIMIT_MB, description: "the most megabytes all runs may take" },
142
+ { key: "sender_colors", default: false, description: "color each sender's name in a conversation" },
143
+ { key: "search_catch_up", default: false, description: "prepare local search after a gaps repair" },
144
+ {
145
+ key: "history_dialog_days",
146
+ default: DEFAULT_HISTORY_LIMITS.dialogDays,
147
+ description: "days of a one-to-one chat's pushed history the store keeps",
148
+ },
149
+ {
150
+ key: "history_group_days",
151
+ default: DEFAULT_HISTORY_LIMITS.groupDays,
152
+ description: "days of a group's pushed history the store keeps",
153
+ },
154
+ {
155
+ key: "history_group_members_max",
156
+ default: DEFAULT_HISTORY_LIMITS.groupMembersMax,
157
+ description: "a group with more people than this keeps no pushed history",
158
+ },
159
+ {
160
+ key: "picture_search",
161
+ default: false,
162
+ description: "search and images embed read the pictures themselves, with a model on this computer",
163
+ },
164
+ {
165
+ key: "search_reranker",
166
+ default: DEFAULT_RERANK_MODEL,
167
+ description: "the reranker that orders search's first hits, or off",
168
+ },
169
+ {
170
+ key: "embedding_provider",
171
+ default: AI_DEFAULTS.embeddingProvider,
172
+ description: "local or openai: what turns text into vectors for search by meaning",
173
+ },
174
+ ];
110
175
  /** Where `profile` came from when none was named and the command needs none. */
111
176
  export const NONE_NAMED = "none named";
112
177
  /** A name for a command that speaks for no account; never chosen for one that does. */
@@ -174,12 +239,18 @@ export const settingsFor = (app, extension = {}) => {
174
239
  const allSettings = [...Object.keys(profileEntries), ...botOnlyKeys, ...defaultsOnlyKeys].filter((setting) => app.messenger !== false ||
175
240
  !MESSENGER_ONLY_SETTINGS.includes(setting) ||
176
241
  (app.sends === true && setting === "sends_per_hour"));
242
+ const readSchema = extension.schema ?? schema;
243
+ // An app with a schema of its own may refuse a shared key in `defaults`; writing it would break that file.
244
+ const declaredSettings = [...SHARED_SETTINGS, ...(extension.settings ?? [])]
245
+ .filter(({ key, default: value }) => allSettings.includes(key) && v.safeParse(readSchema, { defaults: { [key]: value } }).success)
246
+ .map((setting) => ({ ...setting, key: `defaults.${setting.key}` }));
247
+ // `defaults` needs no field of its own, so it is the one section created: every key then shows (RISK-2125).
248
+ const withDefaults = (raw, except = []) => withSettingDefaults(isScope(raw) && !Object.hasOwn(raw, "defaults") ? { ...raw, defaults: {} } : raw, declaredSettings.filter(({ key }) => !except.includes(key)));
177
249
  const PROFILE = envName(app, "PROFILE");
178
250
  const PROFILE_LOCK = envName(app, "PROFILE_LOCK");
179
251
  const TIMEOUT = envName(app, "TIMEOUT");
180
252
  const REQUESTS_PER_MINUTE = envName(app, "REQUESTS_PER_MINUTE");
181
253
  const configPathFor = ({ env = process.env, configDir }) => configFilePath(configDir ?? resolvePaths({ appName: app.appName, prefix: app.envPrefix, env }).config);
182
- const readSchema = extension.schema ?? schema;
183
254
  const context = (file) => ({ file, tool: app.command });
184
255
  /** The file with every problem found; what loads is the file without the settings they name. */
185
256
  const loadConfig = (path) => {
@@ -225,7 +296,7 @@ export const settingsFor = (app, extension = {}) => {
225
296
  const configPath = configPathFor(options);
226
297
  try {
227
298
  if (!options.readOnly)
228
- ensureDefaultConfig(configPath);
299
+ ensureDefaultConfig(configPath, withDefaults(DEFAULT_CONFIG).config);
229
300
  }
230
301
  catch (error) {
231
302
  throw new CliError("configuration_error", error instanceof Error ? error.message : String(error));
@@ -435,7 +506,7 @@ export const settingsFor = (app, extension = {}) => {
435
506
  throw new CliError("validation_error", `${setting} has no problem to remove — name the profile, or add --defaults, to unset a setting`);
436
507
  if (!known && valueAt(raw, at) === undefined)
437
508
  throw new CliError("validation_error", `no setting called "${setting}" — one of: ${allSettings.join(", ")}`);
438
- saveConfigFile(path, withoutPath(raw, at).config);
509
+ saveConfigFile(path, withDefaults(withoutPath(raw, at).config, [at.join(".")]).config);
439
510
  return null;
440
511
  }
441
512
  if (!known)
@@ -456,13 +527,25 @@ export const settingsFor = (app, extension = {}) => {
456
527
  if (added)
457
528
  throw new CliError("validation_error", `${setting} cannot be "${value}": ${added.what}`);
458
529
  const checked = v.safeParse(readSchema, changed);
459
- saveConfigFile(path, checked.success ? checked.output : changed);
530
+ saveConfigFile(path, withDefaults(checked.success ? checked.output : changed).config);
460
531
  return valueAt(checked.success ? checked.output : changed, at) ?? null;
461
532
  };
533
+ /** Each declared setting the file lacks, at its default, on the file as written; the keys added. */
534
+ const migrate = (path) => {
535
+ const read = readConfigFile(path);
536
+ if ("issue" in read)
537
+ throw new CliError("configuration_error", issueMessage([read.issue]));
538
+ const { config, added } = withDefaults(read.raw);
539
+ if (added.length > 0)
540
+ saveConfigFile(path, config);
541
+ return added;
542
+ };
462
543
  return {
463
544
  resolveSettings,
464
545
  configuredProfiles,
465
546
  changeSetting,
547
+ migrate,
548
+ declaredSettings,
466
549
  allSettings,
467
550
  defaultsOnlyKeys,
468
551
  schema,
@@ -8,6 +8,8 @@ export declare const appDoctorCommand: (app: import("./entry.js").AppIdentity, c
8
8
  kind?: import("./settings.js").ProfileKind | undefined;
9
9
  root?: boolean;
10
10
  }) => unknown;
11
+ migrate: (path: string) => string[];
12
+ declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
11
13
  allSettings: string[];
12
14
  defaultsOnlyKeys: string[];
13
15
  schema: import("valibot").StrictObjectSchema<{
@@ -188,6 +190,8 @@ export declare const appMcpCommand: (app: import("./entry.js").AppIdentity, conf
188
190
  kind?: import("./settings.js").ProfileKind | undefined;
189
191
  root?: boolean;
190
192
  }) => unknown;
193
+ migrate: (path: string) => string[];
194
+ declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
191
195
  allSettings: string[];
192
196
  defaultsOnlyKeys: string[];
193
197
  schema: import("valibot").StrictObjectSchema<{
@@ -375,6 +379,8 @@ export declare const configCommand: (app: import("./entry.js").AppIdentity, conf
375
379
  kind?: import("./settings.js").ProfileKind | undefined;
376
380
  root?: boolean;
377
381
  }) => unknown;
382
+ migrate: (path: string) => string[];
383
+ declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
378
384
  allSettings: string[];
379
385
  defaultsOnlyKeys: string[];
380
386
  schema: import("valibot").StrictObjectSchema<{
@@ -590,6 +596,8 @@ export declare const completeCommand: (messenger: import("./index.js").Messenger
590
596
  kind?: import("./settings.js").ProfileKind | undefined;
591
597
  root?: boolean;
592
598
  }) => unknown;
599
+ migrate: (path: string) => string[];
600
+ declaredSettings: import("./setting-defaults.js").DeclaredSetting[];
593
601
  allSettings: string[];
594
602
  defaultsOnlyKeys: string[];
595
603
  schema: import("valibot").StrictObjectSchema<{
@@ -75,4 +75,9 @@ export interface AdminCalculated {
75
75
  graph: ReturnType<typeof rankReplyGraph>["quality"];
76
76
  }
77
77
  export declare const adminEntityKey: (row: Pick<AdminRow, "account" | "id">) => string;
78
+ /** Median and nearest-rank p90 of reply delays, the numbers every response-time answer reports. */
79
+ export declare const delayStats: (delays: number[]) => {
80
+ medianMilliseconds: number | null;
81
+ p90Milliseconds: number | null;
82
+ };
78
83
  export declare const calculateAdminStatistics: (nodes: AdminNode[], stays: AdminStay[], options: AdminOptions, inspect: () => void) => AdminCalculated;
@@ -6,7 +6,8 @@ import { rankingQuestion } from "./rankings-options.js";
6
6
  export const adminEntityKey = (row) => JSON.stringify([row.account.provider, row.account.account, row.id]);
7
7
  const personKey = (node) => JSON.stringify([node.account.provider, node.account.account, node.sender]);
8
8
  const locator = (node) => formatLocator({ ...node.account, chat: node.chatId, message: node.id });
9
- const stats = (delays) => {
9
+ /** Median and nearest-rank p90 of reply delays, the numbers every response-time answer reports. */
10
+ export const delayStats = (delays) => {
10
11
  const sorted = delays.toSorted((a, b) => a - b);
11
12
  const middle = Math.floor(sorted.length / 2);
12
13
  return {
@@ -103,7 +104,7 @@ export const calculateAdminStatistics = (nodes, stays, options, inspect) => {
103
104
  account: { provider: person.provider, account: person.account },
104
105
  questions: eligible.filter((node) => node.account.provider === person.provider && node.account.account === person.account).length,
105
106
  answered: rows.length,
106
- ...stats(rows.map((row) => row.contribution)),
107
+ ...delayStats(rows.map((row) => row.contribution)),
107
108
  status: "observed",
108
109
  }, rows);
109
110
  }
@@ -154,7 +155,7 @@ export const calculateAdminStatistics = (nodes, stays, options, inspect) => {
154
155
  pending: end > options.cutoff,
155
156
  questions: held.length,
156
157
  answered: delays.length,
157
- ...stats(delays),
158
+ ...delayStats(delays),
158
159
  status: "partial",
159
160
  }, contributions);
160
161
  }
@@ -71,6 +71,7 @@ import { reactionChange } from "./reaction-change.js";
71
71
  import { recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved } from "./recipient.js";
72
72
  import { recordedRun, runDetails, runHit, runLocation } from "./recorded-run.js";
73
73
  import { relationship } from "./relationship.js";
74
+ import { responseTimes, waitingChats } from "./reply-stats.js";
74
75
  import { retention } from "./retention.js";
75
76
  import { savedSearch, searchHistoryClear } from "./saved-search.js";
76
77
  import { sendAttempt } from "./send-attempt.js";
@@ -88,7 +89,7 @@ import { topic } from "./topic.js";
88
89
  * subset of its fields, never a renamed one. Commands move onto these one by one; until then, a schema
89
90
  * is the target, not a description of today's output.
90
91
  */
91
- export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reaction, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, storeTable, tagChange, tagging, task, taskAnalysis, taskAnalysisMark, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, topicIcon, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
92
+ export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reaction, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, responseTimes, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, storeTable, tagChange, tagging, task, taskAnalysis, taskAnalysisMark, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, topicIcon, trackedChat, trackedChatHistory, waitingChats, watchedMessage, watchItem, };
92
93
  export declare const ENTITIES: {
93
94
  readonly meeting_ingestion: v.StrictObjectSchema<{
94
95
  readonly meetings: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
@@ -10860,6 +10861,35 @@ export declare const ENTITIES: {
10860
10861
  readonly count: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10861
10862
  }, undefined>, undefined>;
10862
10863
  }, undefined>;
10864
+ readonly response_times: v.StrictObjectSchema<{
10865
+ readonly replies: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10866
+ readonly median_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
10867
+ readonly p90_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
10868
+ readonly mean_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
10869
+ readonly who: v.PicklistSchema<["me", "others"], undefined>;
10870
+ readonly buckets: v.StrictObjectSchema<{
10871
+ readonly under_5m: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10872
+ readonly under_1h: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10873
+ readonly under_1d: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10874
+ readonly over_1d: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10875
+ }, undefined>;
10876
+ readonly rows: v.ArraySchema<v.StrictObjectSchema<{
10877
+ readonly replies: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10878
+ readonly median_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
10879
+ readonly p90_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
10880
+ readonly mean_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
10881
+ readonly key: v.StringSchema<undefined>;
10882
+ }, undefined>, undefined>;
10883
+ }, undefined>;
10884
+ readonly waiting_chats: v.StrictObjectSchema<{
10885
+ readonly chats: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10886
+ readonly messages: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10887
+ readonly oldest_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
10888
+ readonly by_kind: v.StrictObjectSchema<{
10889
+ readonly private: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10890
+ readonly group: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
10891
+ }, undefined>;
10892
+ }, undefined>;
10863
10893
  readonly ranking: v.StrictObjectSchema<{
10864
10894
  readonly target: v.PicklistSchema<["messages", "contacts"], undefined>;
10865
10895
  readonly measure: v.StringSchema<undefined>;
@@ -78,6 +78,7 @@ import { reactionChange } from "./reaction-change.js";
78
78
  import { recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved } from "./recipient.js";
79
79
  import { recordedRun, runDetails, runHit, runLocation } from "./recorded-run.js";
80
80
  import { relationship } from "./relationship.js";
81
+ import { responseTimes, waitingChats } from "./reply-stats.js";
81
82
  import { retention } from "./retention.js";
82
83
  import { savedSearch, searchHistoryClear } from "./saved-search.js";
83
84
  import { sendAttempt } from "./send-attempt.js";
@@ -95,7 +96,7 @@ import { topic } from "./topic.js";
95
96
  * subset of its fields, never a renamed one. Commands move onto these one by one; until then, a schema
96
97
  * is the target, not a description of today's output.
97
98
  */
98
- export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reaction, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, storeTable, tagChange, tagging, task, taskAnalysis, taskAnalysisMark, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, topicIcon, trackedChat, trackedChatHistory, watchedMessage, watchItem, };
99
+ export { account, accountChange, accountPrivacy, accountPrivacyChange, accountProfile, accountSession, adminReport, agenda, analysisConsent, analysisConsentRevocation, attachment, attachmentBytes, attachmentExtraction, attachmentTextChange, automaticTags, botAdmin, botMenuEntry, botProfile, botSeenChat, botTemplate, botWebhook, cachedChatMetadata, callRecord, chart, chat, chatAdminChange, chatArchive, chatEvent, chatFolderChange, chatFolderOrder, chatFolderRemoval, chatGroupChange, chatInspection, chatInvite, chatInviteChange, chatMembersChange, chatMembersFetch, chatMetadata, chatMetadataRefresh, chatModeration, chatMuting, chatPin, chatRead, chatRemoval, chatRequestChange, chatRules, chatSender, chatStats, chatTracking, chatUnread, contact, contactActivity, contactAlias, contactCheck, contactContext, contactImport, contactLookup, contactNote, contactNoteRemoval, contactPreparation, contactStateChange, contactSync, contactUpdate, conversation, conversationBatch, conversationBatchStatus, conversationBuildResult, conversationChange, conversationDetails, conversationEmbedding, conversationEmbeddingStatus, conversationHit, conversationPreparation, conversationReadiness, counterRefresh, counters, document, documentLink, endedAccountSessions, evidence, evidencePacket, fact, folder, hit, identity, imageEmbedding, inviteLink, job, joinRequest, linkedPerson, mediaArchive, mediaArchiveCheck, mediaArchiveMove, mediaArchiveTimer, meeting, meetingProjectCandidates, meetingSeriesInventory, member, memory, mention, mentionDecision, mentionDetails, mentionDomain, mentionRebuild, message, messageContext, messageCounts, messageDeletion, messageDownload, messageEvent, messageLink, messageLinks, messagePin, messageStar, messageSync, messageTranscript, messageWrite, model, modelKey, note, officialStats, organization, person, place, poll, pollChange, pollVote, project, ranking, reaction, reactionChange, recipient, recipientAdded, recipientChange, recipientOff, recipientRemoved, recordedRun, reference, relationship, reminder, removedModelKey, responseTimes, retention, runDetails, runHit, runLocation, savedSearch, scheduledJob, searchHistoryClear, sendAttempt, serveSummary, stickerInventory, storeBackup, storeChatStatus, storeCheck, storeClear, storeCopyRemoval, storeDecrypt, storedModelKey, storeExport, storeGapPlan, storeGapRepair, storeInfo, storeJobCancel, storeJobClear, storeJobDetails, storeJobRetry, storeMigration, storeReindex, storeRepair, storeReset, storeRestore, storeTable, tagChange, tagging, task, taskAnalysis, taskAnalysisMark, taskCard, taskCategories, taskClosed, taskCreated, taskLinks, taskMerge, taskPair, taskScan, taskSimilar, taskStats, taskUnmerge, topic, topicIcon, trackedChat, trackedChatHistory, waitingChats, watchedMessage, watchItem, };
99
100
  export const ENTITIES = {
100
101
  sync_progress: syncProgress,
101
102
  ...MEETING_RESULT_ENTITIES,
@@ -348,6 +349,8 @@ export const ENTITIES = {
348
349
  topic,
349
350
  folder,
350
351
  message_counts: messageCounts,
352
+ response_times: responseTimes,
353
+ waiting_chats: waitingChats,
351
354
  ranking,
352
355
  admin_report: adminReport,
353
356
  analysis_consent: analysisConsent,
@@ -0,0 +1,33 @@
1
+ import * as v from "valibot";
2
+ /** How long replies took in one-to-one chats: the owner's (`me`) or the other side's (`others`). */
3
+ export declare const responseTimes: v.StrictObjectSchema<{
4
+ readonly replies: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
5
+ readonly median_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
6
+ readonly p90_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
7
+ readonly mean_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
8
+ readonly who: v.PicklistSchema<["me", "others"], undefined>;
9
+ readonly buckets: v.StrictObjectSchema<{
10
+ readonly under_5m: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
11
+ readonly under_1h: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
12
+ readonly under_1d: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
13
+ readonly over_1d: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
14
+ }, undefined>;
15
+ /** One row per calendar day of the reply; empty with `--by total`. */
16
+ readonly rows: v.ArraySchema<v.StrictObjectSchema<{
17
+ readonly replies: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
18
+ readonly median_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
19
+ readonly p90_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
20
+ readonly mean_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
21
+ readonly key: v.StringSchema<undefined>;
22
+ }, undefined>, undefined>;
23
+ }, undefined>;
24
+ /** Chats whose newest message asks the owner something, and how much waits there. */
25
+ export declare const waitingChats: v.StrictObjectSchema<{
26
+ readonly chats: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
27
+ readonly messages: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
28
+ readonly oldest_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
29
+ readonly by_kind: v.StrictObjectSchema<{
30
+ readonly private: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
31
+ readonly group: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
32
+ }, undefined>;
33
+ }, undefined>;
@@ -0,0 +1,19 @@
1
+ import * as v from "valibot";
2
+ import { count, text, time } from "./fields.js";
3
+ const milliseconds = v.nullable(v.pipe(v.number(), v.minValue(0)));
4
+ const figures = { replies: count, median_ms: milliseconds, p90_ms: milliseconds, mean_ms: milliseconds };
5
+ /** How long replies took in one-to-one chats: the owner's (`me`) or the other side's (`others`). */
6
+ export const responseTimes = v.strictObject({
7
+ who: v.picklist(["me", "others"]),
8
+ ...figures,
9
+ buckets: v.strictObject({ under_5m: count, under_1h: count, under_1d: count, over_1d: count }),
10
+ /** One row per calendar day of the reply; empty with `--by total`. */
11
+ rows: v.array(v.strictObject({ key: text, ...figures })),
12
+ });
13
+ /** Chats whose newest message asks the owner something, and how much waits there. */
14
+ export const waitingChats = v.strictObject({
15
+ chats: count,
16
+ messages: count,
17
+ oldest_at: v.nullable(time),
18
+ by_kind: v.strictObject({ private: count, group: count }),
19
+ });
@@ -7,7 +7,7 @@ export declare const REASONS: ErrorReason[];
7
7
  * Detail keys an error from this package may carry, until each is registered on its own reason. Not `cause`:
8
8
  * that can hold a provider's or SQLite's raw text.
9
9
  */
10
- export declare const DETAILS: readonly ["contracts", "calendars", "allowed", "alternative", "arguments", "at", "available", "batch", "budget", "built", "candidates", "chat", "chat_id", "cli", "column", "complete", "emitted_bytes", "exact", "file", "forum", "guide", "hint", "id", "index", "kind", "limit", "limit_source", "line", "max_bytes", "model", "name", "operation_id", "operation_ids", "text_limit", "store_refused_in", "subtasks", "partial_output", "parent", "path", "permission", "person_ids", "previous_chat_id", "profiles", "provider_error", "provider", "recovery", "reference", "remembered", "retry_at", "retry_after_ms", "runtime", "saved", "scheduled_for", "script", "sent", "sent_count", "settlement_failed", "span", "sqlite", "stage", "standing", "state", "stderr", "term", "total", "upgraded", "wanted"];
10
+ export declare const DETAILS: readonly ["contracts", "calendars", "allowed", "alternative", "arguments", "at", "available", "batch", "budget", "built", "candidates", "chat", "chat_id", "cli", "column", "complete", "emitted_bytes", "exact", "file", "forum", "guide", "hint", "id", "index", "kind", "limit", "limit_source", "line", "max_bytes", "model", "name", "operation_id", "operation_ids", "text_limit", "store_refused_in", "partial_output", "parent", "path", "permission", "person_ids", "previous_chat_id", "profiles", "provider_error", "provider", "recovery", "reference", "remembered", "retry_at", "retry_after_ms", "runtime", "saved", "scheduled_for", "script", "sent", "sent_count", "settlement_failed", "span", "sqlite", "stage", "standing", "state", "stderr", "term", "total", "upgraded", "wanted"];
11
11
  /** What a CLI adds to the shared registry: its own reasons, and the detail keys its adapters raise. */
12
12
  export interface AppErrors {
13
13
  reasons?: readonly ErrorReason[];
@@ -47,6 +47,7 @@ const OTHERS = [
47
47
  validation("mail-findings.stale", "mail findings cite mail that changed since it was read; stage a new batch"),
48
48
  validation("mail-findings.partial", "a cited email is not stored whole yet; cite it once it is"),
49
49
  validation("evidence-findings.dedicated-review", "review evidence findings through the dedicated findings command"),
50
+ validation("mail-findings.dedicated-review", "review mail findings through the dedicated findings command"),
50
51
  validation("document.changed", "the document changed while it was read; nothing was saved"),
51
52
  ["document.not-imported", "not_found", "the document is not imported in this source account"],
52
53
  ["extraction.invalid-proposals", "invalid_response", "the model's memory proposals could not be used"],
@@ -218,8 +219,7 @@ const OTHERS = [
218
219
  ["task.stale", "configuration_error", "the task changed since it was read; read it again"],
219
220
  validation("task.column", "the task beside which it moves is in another column than the status asked for"),
220
221
  validation("task.dependency-cycle", "the task would depend on itself"),
221
- validation("task.has-subtasks", "the task has subtasks; move or detach them before it moves to another project"),
222
- validation("task.is-subtask", "a subtask moves to another project only when it is detached from its parent in the same write"),
222
+ validation("task.is-subtask", "a subtask moves to another project alone only when it is detached from its parent in the same write"),
223
223
  validation("task.duplicate-candidates", "open tasks may be the same job: attach to one, or add it as new"),
224
224
  validation("task.already-read", "the message was already read for tasks: a read again only updates and closes tasks unless --open-new"),
225
225
  validation("task.certainty-required", "an agent closes a task with its certainty, 0 to 1"),
@@ -300,7 +300,6 @@ export const DETAILS = [
300
300
  "operation_ids",
301
301
  "text_limit",
302
302
  "store_refused_in",
303
- "subtasks",
304
303
  "partial_output",
305
304
  "parent",
306
305
  "path",
@@ -3,14 +3,27 @@ import type { AttachmentInput, EmailAddress } from "../store/index.js";
3
3
  import type { Address, ParsedMail } from "./parse.js";
4
4
  import type { MailSyncResult } from "./sync.js";
5
5
  export declare const MAX_ATTACHMENT_CHARS = 200000;
6
+ /** Whether a sync reads an email's attachments into the index, and the largest file it reads. */
7
+ export interface MailAttachmentPolicy {
8
+ read: boolean;
9
+ maxBytes: number;
10
+ }
11
+ /** On, up to 10 MB a file: ordinary documents and scans are read, a large archive or video is not. */
12
+ export declare const DEFAULT_ATTACHMENT_POLICY: MailAttachmentPolicy;
13
+ /** Why a policy left an attachment unread; `readEmailWhole` reads it anyway. */
14
+ export declare const POLICY_SKIPS: readonly ["not-read", "too-large"];
6
15
  /** One chunk of raw mail is held in memory at a time. */
7
16
  export declare const FETCH_CHUNK = 50;
8
17
  export declare const addressOf: ({ email, name }: Address) => EmailAddress;
9
18
  export type AttachmentLog = NonNullable<MailSyncResult["attachments"]>;
10
- /** A parsed email's attachments as the store keeps them, each with its text or why it has none. */
11
- export declare const readAttachments: (mail: ParsedMail, messageId: string, { loadEngine, maxAttachmentChars, at, signal, }: {
19
+ /**
20
+ * A parsed email's attachments as the store keeps them, each with its text or why it has none. Without `policy` every
21
+ * attachment is read, whatever its size.
22
+ */
23
+ export declare const readAttachments: (mail: ParsedMail, messageId: string, { loadEngine, maxAttachmentChars, at, signal, policy, }: {
12
24
  loadEngine: LoadEngine;
13
25
  maxAttachmentChars: number;
14
26
  at: number;
15
27
  signal?: AbortSignal;
28
+ policy?: MailAttachmentPolicy;
16
29
  }, log: AttachmentLog) => Promise<AttachmentInput[]>;
@@ -1,17 +1,33 @@
1
1
  import { extractText } from "../attachments/extract.js";
2
2
  export const MAX_ATTACHMENT_CHARS = 200_000;
3
+ /** On, up to 10 MB a file: ordinary documents and scans are read, a large archive or video is not. */
4
+ export const DEFAULT_ATTACHMENT_POLICY = { read: true, maxBytes: 10 * 1024 * 1024 };
5
+ /** Why a policy left an attachment unread; `readEmailWhole` reads it anyway. */
6
+ export const POLICY_SKIPS = ["not-read", "too-large"];
3
7
  /** One chunk of raw mail is held in memory at a time. */
4
8
  export const FETCH_CHUNK = 50;
5
9
  export const addressOf = ({ email, name }) => ({ address: email, name });
6
- /** A parsed email's attachments as the store keeps them, each with its text or why it has none. */
7
- export const readAttachments = async (mail, messageId, { loadEngine, maxAttachmentChars, at, signal, }, log) => {
10
+ /**
11
+ * A parsed email's attachments as the store keeps them, each with its text or why it has none. Without `policy` every
12
+ * attachment is read, whatever its size.
13
+ */
14
+ export const readAttachments = async (mail, messageId, { loadEngine, maxAttachmentChars, at, signal, policy, }, log) => {
8
15
  const files = [];
9
16
  for (const [position, part] of mail.attachments.entries()) {
10
17
  if (signal?.aborted)
11
18
  throw signal.reason;
19
+ const skipped = part.bytes === null || policy === undefined
20
+ ? undefined
21
+ : !policy.read
22
+ ? "not-read"
23
+ : part.bytes.byteLength > policy.maxBytes
24
+ ? "too-large"
25
+ : undefined;
12
26
  const extraction = part.bytes === null
13
27
  ? { status: "unavailable" }
14
- : await extractText(part.bytes, { kind: "file", name: part.name, mime: part.mime, path: part.name }, loadEngine, signal);
28
+ : skipped
29
+ ? { status: skipped }
30
+ : await extractText(part.bytes, { kind: "file", name: part.name, mime: part.mime, path: part.name }, loadEngine, signal);
15
31
  log.push({ message: messageId, part: part.id, status: extraction.status });
16
32
  const extracted = extraction.status === "extracted" ? extraction : undefined;
17
33
  files.push({
@@ -3,7 +3,22 @@ import { type MailServerAccess } from "../../mail/server-search.js";
3
3
  import type { AppIdentity } from "../../ports/app.js";
4
4
  import { type AccountKey, type MessageStore } from "../../store/store.js";
5
5
  export { mailMark, markItem, markText } from "./marks.js";
6
- export { MAIL_REVIEW_QUERY, MAIL_REVIEW_STEPS } from "./review.js";
6
+ export { MAIL_REVIEW_QUERY, MAIL_REVIEW_STEPS, mailReviewSteps } from "./review.js";
7
+ /**
8
+ * Where a host mounted `review`, `messages acknowledge` and `conversations show`, and the option that names one
9
+ * mailbox, so the steps a review hands on run there. A mail CLI leaves it out: its profile names the mailbox.
10
+ */
11
+ export interface MailMount {
12
+ review?: string;
13
+ acknowledge?: string;
14
+ /** `null`: the host mounts no thread read, so a review offers none. */
15
+ conversation?: string | null;
16
+ /** In snake_case, as a step names it: memo's `source`, with `email:<account>`. */
17
+ mailbox?: {
18
+ option: string;
19
+ value: (mailbox: AccountKey) => string;
20
+ };
21
+ }
7
22
  /** What a mail CLI hands the shared commands: how to open the store for one run, and whose mail it reads. */
8
23
  export interface MailReads {
9
24
  app: AppIdentity;
@@ -14,6 +29,7 @@ export interface MailReads {
14
29
  read: <T>(command: Command, work: (store: MessageStore, mailbox: AccountKey) => Promise<T>) => Promise<T>;
15
30
  /** The mailbox's server, for `search messages --backend`; without it the search reads the store only. */
16
31
  server?: (command: Command) => MailServerAccess;
32
+ mount?: MailMount;
17
33
  /** How `--server-time` help states its default when a provider's own differs (`MailServerAccess.timeMs`). */
18
34
  serverTimeDefault?: string;
19
35
  }
@@ -21,7 +21,7 @@ import { queryFilter, THREAD_COLUMNS } from "../../store/store.js";
21
21
  import { acknowledgeCommand } from "./marks.js";
22
22
  import { mailReviewCommand } from "./review.js";
23
23
  export { mailMark, markItem, markText } from "./marks.js";
24
- export { MAIL_REVIEW_QUERY, MAIL_REVIEW_STEPS } from "./review.js";
24
+ export { MAIL_REVIEW_QUERY, MAIL_REVIEW_STEPS, mailReviewSteps } from "./review.js";
25
25
  /** A locked or broken store must fail the command, not read as "nothing stored yet". */
26
26
  const absentAccount = (error) => {
27
27
  if (error instanceof CliError && error.code === "not_found")
@@ -1,8 +1,11 @@
1
+ import { type StepDefinition } from "@wirecat/cli-base/standards";
1
2
  import { Command } from "commander";
2
- import type { MailReads } from "./index.js";
3
+ import type { MailMount, MailReads } from "./index.js";
3
4
  /** Where a mail review starts: a time only; one thread is `conversations show`. */
4
5
  export declare const MAIL_REVIEW_QUERY: import("@wirecat/cli-base/query").QueryRegistry;
5
- /** The mail review's steps; a host lists them beside its own so its step checks know them. */
6
+ /** The mail review's steps where a host mounted them; a host lists them beside its own so its step checks know them. */
7
+ export declare const mailReviewSteps: (mount?: MailMount) => StepDefinition[];
8
+ /** The steps of a mail CLI, whose profile names the mailbox. */
6
9
  export declare const MAIL_REVIEW_STEPS: ({
7
10
  id: string;
8
11
  kind: "command";