@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
package/CHANGELOG.md CHANGED
@@ -5,6 +5,145 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.564.0 — 05.11.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **`search all` ranks its sources as one list** (MEMO-37). Each source's hit at one rank used to get the same score,
15
+ so the sources took turns in the order they were read (messages, mail, notes, events). Now hits at the same rank
16
+ are ordered by cosine, since one model scores every source; a hit meaning did not find comes after those it did.
17
+ Each source's own order is kept. Without the model nothing changes. `search notes` and `search events` hits found
18
+ by meaning carry `meaning_score`, the cosine.
19
+ - **tg, max and wa `search all` change with the bump, with no code change in them.** Mail is searched by meaning when
20
+ the store holds mail, a question's message found both by words and by meaning gets one vote, and once vectors
21
+ exist the hits at one rank are ordered by meaning score.
22
+ - **`search all` searches mail by meaning too** (MEMO-38). Mail was only ever searched by words there, though its
23
+ threads had vectors. The meaning lane now reads a mail account's thread vectors through the mail store, and
24
+ `search all` fuses them with the word hits as it does for messages; the answer says how in `mail`. For a question,
25
+ a mail or message found by words gets no second vote from meaning, as in notes.
26
+ - **`search all` finds records for a question typed as a sentence.** When the text is a question in natural language
27
+ and not `exact`, messages and mail go through discovery (as `search messages --discover`), and notes and calendar
28
+ events search any of the question's content words, ranked by the store. Before, every word had to be in one record,
29
+ so a question found nothing (MEMO-36). tg, max and wa still ask the messenger's server with the question as typed.
30
+ For a question, a note found by both words and meaning gets one vote, not two, so notes found by words alone are not
31
+ buried. Plain words, query syntax and `exact` are searched strictly, as before. `searchNotes` and `searchEvents`
32
+ take `discover`.
33
+
34
+ ### Added
35
+
36
+ - **The proposal registry reviews meeting findings and task close suggestions.** `proposalRegistry` registers
37
+ `meeting_findings` (a meeting's staged batch, decided part by part as in `meetings actions confirm`) and
38
+ `task_close` (an agent's suggested close, id `close:<task key>`; confirming closes the task as suggested, rejecting
39
+ clears the suggestion). Both are owner only: `isOwnerOnlyKind` now names `task_close` too, so `mayDecide` refuses an
40
+ agent by the row's kind. `parseProposalIds` takes a task key after a prefix (`close:Q-15`). Moved from cli-memo,
41
+ which registered both itself; a host that registers a kind of the same name now gets `configuration_error`.
42
+
43
+ - **A mail CLI's `messages show` can read an email again when a sync's limit left its attachment unread.**
44
+ `mailReadCommands` takes `whole(command, store, mailbox, emailId)`; with it the command is a fetch, reads such an
45
+ email from the server before it answers (`readEmailWhole`), and shows the new text. When that read fails it shows
46
+ the stored email with `meta.complete: false` and an issue `mail.attachments-unread`. Without `whole` nothing changes.
47
+ `readEmailWhole`'s body and text limits, reader and clock now default as sync's do.
48
+ - **`has:transcript` in the messages query language** finds a voice message that has a transcription, so
49
+ `stats messages show --by day -- has:transcript date>=30d` counts transcribed voice messages per day. In mail it
50
+ finds nothing.
51
+ - **`store chats status`** counts the account's searchable chats of every kind, channels included, by how much of each
52
+ the archive holds: `{ chats, fetched, partial, never, oldest_message_at }`. `fetched` is held back to the first
53
+ message, up to date and without gaps; `never` has no fetch recorded and no message held; the rest is `partial`,
54
+ including WhatsApp chats whose history was pushed. One statement over the store, no list of chats. tg, max and wa
55
+ get it with the bump.
56
+ - **Meeting counts per day, week or series.** `readMeetingCounts` (services) and `store.meetings.meetingCounts`
57
+ answer meetings, minutes, mean minutes and meetings with a transcript, in total and per row, over the same
58
+ selection as `readMeetingStats`, reading `meetings` alone through `meetings_by_time`. `meetingCountsItem` and the
59
+ `meeting_counts` entity are the answer, for zoom-cli's `stats meetings history`. A meeting with no end counts in
60
+ `meetings`, not in `minutes`. `MeetingStatsStore` gains `meetingCounts`; only cli-core's own store implements it.
61
+ - **`stats tasks show` counts by chat, meeting or project, over a period.** `--by meeting` keys a row by the meeting a
62
+ task came from (`meeting:<account>/<meeting>`); `--by project` reads the owner's project tasks, every project
63
+ (`project:<id>`). A query after `--` takes the task query's fields and `created` for the period: `created` counts
64
+ the tasks opened in it, `closed` those closed in it, `open` is always open now (`-- created>=30d`). The item gains
65
+ `created`, `closed`, `median_close_ms` and `mean_open_age_ms`; each row gains `key`, `name` (chat title, meeting
66
+ title, project name), `created`, `closed` and `mean_open_age_ms`. Rows of `--by chat` keep `chat`; the others have
67
+ none. `--timezone` places calendar days such as `created>=today`.
68
+ - **`created:` in the project task query** (`PROJECT_TASK_FIELDS`), beside `closed:`: `created>=30d`.
69
+ - **`mailStats` from `./mail`**: stored mail received and sent per calendar day, week (by its Monday) or sender, for
70
+ the `search mail` query, counts only and no search recorded; `mailStatsItem` turns it into the new `mail_counts`
71
+ entity, `{ by, received, sent, rows: [{ key, name, received, sent }] }`. By sender it counts received mail only.
72
+ - **`mailOwnText(body)` (`@wirecat/cli-core/mail`): what an email says itself, without quoted earlier mail and the
73
+ signature.** It drops `>` lines, cuts at a reply header ("On … wrote:", "Le … a écrit :", "Am … schrieb …:",
74
+ "… написал(а):", Gmail's Russian "<address>:") and at an Outlook From/Sent block in English, German, French, Spanish
75
+ and Russian, and drops the `-- ` signature, sign-offs and mobile footers. A forwarded message stays whole; an email
76
+ that is only a quote keeps its body.
77
+
78
+ ### Security
79
+
80
+ - **`review --holder` gives each email's own text** (`mailOwnText`), so an agent's model does not read earlier mail
81
+ or signatures. The fingerprint a finding cites is still taken over the stored body; other reads show the whole text.
82
+ - **Review and inbox items say which fields others wrote** (WC-SECR-21): `summaryOf` items, and so every catch-up
83
+ answer and each mail `review` item, carry `untrusted` (`SUMMARY_UNTRUSTED`: chat title, sender name, text,
84
+ transcript, attachment names); a mail review item adds the email's subject and the names in its headers, as search
85
+ hits do. The `message.summary` entity takes the field.
86
+
87
+ ## 0.563.0 — 03.11.2026
88
+
89
+ ### Changed — may break callers
90
+
91
+ - **Mail sync reads attachments up to 10 MB a file by default, and can skip them.** Until now every attachment was
92
+ read, whatever its size; at the next bump a file over 10 MB is no longer read into the index unless the caller
93
+ passes a policy. `MailSyncOptions.attachments` takes a `MailAttachmentPolicy` (`read`, `maxBytes`); left out it is
94
+ `DEFAULT_ATTACHMENT_POLICY`. A file the policy leaves is stored with its name, type and size but no text, with
95
+ `extractionError` `not-read` or `too-large` (`POLICY_SKIPS`), and the run's `attachments` log says the same.
96
+ Partial completion and `retryAttachments` follow it too.
97
+ - **Moving a task to another project moves its subtasks with it.** `tasks.edit(id, { project })` on a task with
98
+ subtasks no longer refuses with `task.has-subtasks`; that reason is gone. Every subtask, all the way down, takes the
99
+ next key in the new project and keeps its parent, and its old key still finds it. The answer names them as
100
+ `movedSubtasks: [{ from, to }]`, old key and new; the field is left out when nothing moved with the task. A subtask
101
+ moved on its own still needs `parent: null` in the same edit (`task.is-subtask`).
102
+
103
+ - **Owner-only proposals refuse an agent on every decide path.** `mayDecide` and `assertMayDecide` now read
104
+ the row's stored `kind`: a `mail_findings` or `meeting_findings` row is the owner's alone, whatever
105
+ `agent_decisions` says. The registry's `get` and `list` rows carry `ownerOnly` from their kind, and the generic
106
+ action kind marks a stored findings row too, so a host checking `assertMayDecide(await registry.get(id), …)` is
107
+ refused. `approveAsTask` and `proposedActions.approve` / `reject` refuse a mail findings batch as they refuse
108
+ meeting and evidence findings (`mail-findings.dedicated-review`), and the generic action kind no longer lists any
109
+ findings batch. Meeting findings were open to another agent with a reason before; now only the owner reviews them.
110
+
111
+ ### Added
112
+
113
+ - **An answer over the output cap says what to run instead (WC-OUT-22).** The `output.too-large` failure now names up
114
+ to two reads, both safe to run unasked: the same command line with `--compact`, when the command declares a row and
115
+ was not compact already, and the same line with a `--limit` scaled down to fit. The lines are rebuilt from the
116
+ words the caller typed, never from the answer. The failure also carries `needed_bytes`.
117
+ - **A JSON answer over 256 KiB adds one step to the compact form.** The answer is still printed whole. Nothing is
118
+ added when the answer already names two steps, over MCP, or for a command with no declared row.
119
+ - **`search messages --chat` and `search all --chat` say how to search one chat:** `chat:<chat>` in the query.
120
+
121
+ - **`readEmailWhole` reads one stored email again from its mail server, every attachment of it with no limit.** It
122
+ saves the new text on the email as a sync does, keeping its mailboxes, flags and rule, and answers each attachment's
123
+ bytes for the caller to save. Gmail's email is found by id, another server's at its stored place.
124
+ `store.mail.emailPlace(accountId, externalId)` gives an email's thread and stored places.
125
+ - **`stats messages responses`**: how fast replies come in one-to-one chats, the owner's (`--who me`) or the other
126
+ side's (`--who others`): count, median, p90, mean and four time buckets, in total or `--by day` in `--timezone`.
127
+ Several messages in a row followed by one answer are one reply, timed from the first. Reads by index, so all time
128
+ works on a large store; the query after `--` takes `date:`, `chat:` and `kind:` only.
129
+ - **`stats messages waiting`**: how many chats wait on the owner — a one-to-one chat whose newest message is a
130
+ question, or a group whose newest message mentions or replies to the owner — with the messages since the owner
131
+ last wrote, the oldest of them, and the split by kind. Muted or archived one-to-one chats are left out, as in
132
+ `inbox`.
133
+ - **tg, max, wa, mail, gcal and drive's config file holds every shared setting with its default.** A first run now
134
+ writes all of them into `defaults`, not five; `config set` and `config unset` add the missing ones (but not the one
135
+ just unset); and the new `<cli> config migrate` fills an existing file. Your values, their order and unknown keys
136
+ stay; a file without `defaults` gets that section, and no other section is created. `config show` gives each of
137
+ these settings its `default` and `description`, and no longer creates the file when there is none. `settingsFor`
138
+ returns `declaredSettings` and `migrate(path)`; `extension.settings` declares a CLI's own (cli-base WC-CMD-30).
139
+ Settings with no single default (`timeout_ms`, `color`, `requests_per_minute`, the embedding model) and objects
140
+ (`permissions`, `models`) are left out.
141
+ - **`MailReads.mount`: the steps a mail review hands on name where the host mounted it.** A host that mounts `review`
142
+ and `messages acknowledge` elsewhere passes their paths and the option naming the mailbox — memo passes
143
+ `review: "mail review"`, `conversation: null` (it mounts no thread read) and `mailbox: { option: "source", value }`
144
+ — and `next` names those paths and `--source email:<account>`. `mailReviewSteps(mount)` gives the definitions for
145
+ a host's step checks; a mail CLI leaves it out, and its step lines carry the profile as before.
146
+
8
147
  ## 0.562.0 — 03.11.2026
9
148
 
10
149
  ### Added
@@ -10,13 +10,13 @@ export declare const calendarTools: ({
10
10
  inputSchema: {
11
11
  type: string;
12
12
  properties: {
13
+ limit?: undefined;
13
14
  account_id: {
14
15
  type: string;
15
16
  minimum: number;
16
17
  };
17
18
  query?: undefined;
18
19
  timezone?: undefined;
19
- limit?: undefined;
20
20
  page?: undefined;
21
21
  event?: undefined;
22
22
  } | {
@@ -45,9 +45,9 @@ export declare const calendarTools: ({
45
45
  };
46
46
  event?: undefined;
47
47
  } | {
48
+ limit?: undefined;
48
49
  query?: undefined;
49
50
  timezone?: undefined;
50
- limit?: undefined;
51
51
  page?: undefined;
52
52
  event: {
53
53
  type: string;
@@ -58,9 +58,9 @@ export declare const calendarTools: ({
58
58
  minimum: number;
59
59
  };
60
60
  } | {
61
+ limit?: undefined;
61
62
  query?: undefined;
62
63
  timezone?: undefined;
63
- limit?: undefined;
64
64
  page?: undefined;
65
65
  event?: undefined;
66
66
  account_id?: undefined;
@@ -80,13 +80,13 @@ export declare const calendarTools: ({
80
80
  inputSchema: {
81
81
  type: string;
82
82
  properties: {
83
+ limit?: undefined;
83
84
  account_id: {
84
85
  type: string;
85
86
  minimum: number;
86
87
  };
87
88
  query?: undefined;
88
89
  timezone?: undefined;
89
- limit?: undefined;
90
90
  page?: undefined;
91
91
  event?: undefined;
92
92
  } | {
@@ -115,9 +115,9 @@ export declare const calendarTools: ({
115
115
  };
116
116
  event?: undefined;
117
117
  } | {
118
+ limit?: undefined;
118
119
  query?: undefined;
119
120
  timezone?: undefined;
120
- limit?: undefined;
121
121
  page?: undefined;
122
122
  event: {
123
123
  type: string;
@@ -128,9 +128,9 @@ export declare const calendarTools: ({
128
128
  minimum: number;
129
129
  };
130
130
  } | {
131
+ limit?: undefined;
131
132
  query?: undefined;
132
133
  timezone?: undefined;
133
- limit?: undefined;
134
134
  page?: undefined;
135
135
  event?: undefined;
136
136
  account_id?: undefined;
@@ -150,13 +150,13 @@ export declare const calendarTools: ({
150
150
  inputSchema: {
151
151
  type: string;
152
152
  properties: {
153
+ limit?: undefined;
153
154
  account_id: {
154
155
  type: string;
155
156
  minimum: number;
156
157
  };
157
158
  query?: undefined;
158
159
  timezone?: undefined;
159
- limit?: undefined;
160
160
  page?: undefined;
161
161
  event?: undefined;
162
162
  } | {
@@ -185,9 +185,9 @@ export declare const calendarTools: ({
185
185
  };
186
186
  event?: undefined;
187
187
  } | {
188
+ limit?: undefined;
188
189
  query?: undefined;
189
190
  timezone?: undefined;
190
- limit?: undefined;
191
191
  page?: undefined;
192
192
  event: {
193
193
  type: string;
@@ -198,9 +198,9 @@ export declare const calendarTools: ({
198
198
  minimum: number;
199
199
  };
200
200
  } | {
201
+ limit?: undefined;
201
202
  query?: undefined;
202
203
  timezone?: undefined;
203
- limit?: undefined;
204
204
  page?: undefined;
205
205
  event?: undefined;
206
206
  account_id?: undefined;
@@ -220,13 +220,13 @@ export declare const calendarTools: ({
220
220
  inputSchema: {
221
221
  type: string;
222
222
  properties: {
223
+ limit?: undefined;
223
224
  account_id: {
224
225
  type: string;
225
226
  minimum: number;
226
227
  };
227
228
  query?: undefined;
228
229
  timezone?: undefined;
229
- limit?: undefined;
230
230
  page?: undefined;
231
231
  event?: undefined;
232
232
  } | {
@@ -255,9 +255,9 @@ export declare const calendarTools: ({
255
255
  };
256
256
  event?: undefined;
257
257
  } | {
258
+ limit?: undefined;
258
259
  query?: undefined;
259
260
  timezone?: undefined;
260
- limit?: undefined;
261
261
  page?: undefined;
262
262
  event: {
263
263
  type: string;
@@ -268,9 +268,9 @@ export declare const calendarTools: ({
268
268
  minimum: number;
269
269
  };
270
270
  } | {
271
+ limit?: undefined;
271
272
  query?: undefined;
272
273
  timezone?: undefined;
273
- limit?: undefined;
274
274
  page?: undefined;
275
275
  event?: undefined;
276
276
  account_id?: undefined;
@@ -30,6 +30,7 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
30
30
  const bot = this.opts().bot === true;
31
31
  const context = baseContext(this, (flags, options) => config.resolveSettings(flags, {
32
32
  ...options,
33
+ readOnly: true,
33
34
  needsProfile: false,
34
35
  withIssues: true,
35
36
  ...(bot ? { kind: "bot" } : {}),
@@ -48,7 +49,13 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
48
49
  paths_overridden: overridden,
49
50
  settings: [...config.allSettings, "command_timeout_ms"]
50
51
  .filter((setting) => bot || setting !== "read_other_bots")
51
- .map((setting) => sourced(settings, setting)),
52
+ .map((setting) => {
53
+ const declared = config.declaredSettings.find(({ key }) => key === `defaults.${setting}`);
54
+ return {
55
+ ...sourced(settings, setting),
56
+ ...(declared ? { default: declared.default, description: declared.description } : {}),
57
+ };
58
+ }),
52
59
  store_settings: await storeSettings(env),
53
60
  }));
54
61
  for (const issue of [
@@ -144,9 +151,19 @@ export const configCommand = (app, config, { permissionKey = config.permissions,
144
151
  }));
145
152
  });
146
153
  }
154
+ command.addCommand(migrateCommand(app, config));
147
155
  command.addCommand(repairCommand(app, config));
148
156
  return command;
149
157
  };
158
+ const migrateCommand = (app, config) => profileFree(grantsAuthority(annotate(new Command("migrate"), { effects: LOCAL_WRITE, examples: [`${app.command} config migrate`] })))
159
+ .description("write each setting the file lacks at its default, keeping yours")
160
+ .action(function () {
161
+ const context = baseContext(this, (flags, options) => config.resolveSettings(flags, { ...options, needsProfile: false, withIssues: true }));
162
+ const file = context.settings.configPath;
163
+ const added = config.migrate(file);
164
+ renderAnswer(this, context, itemOf({ config_file: file, added }));
165
+ context.renderer.note(added.length > 0 ? `added ${added.join(", ")} to ${file}` : `${file} already has every setting`);
166
+ });
150
167
  /**
151
168
  * Mends JSON syntax with one reading and keeps every value (NEED-2178): no network, no AI, since the file may hold
152
169
  * keys. Anything else is refused with nothing changed.
@@ -5,6 +5,7 @@ import * as v from "valibot";
5
5
  import { currentRun } from "../runs/scope.js";
6
6
  import { compactText, entityRows, rowShapeOf, rowsOfEntity, TEXT_CHARS } from "./compact.js";
7
7
  import { environmentOf, setExitCode } from "./context.js";
8
+ import { largeAnswerSteps, SOFT_OUTPUT_BYTES } from "./output-budget.js";
8
9
  import { withoutProviderRefs } from "./provider-refs.js";
9
10
  import { nextStep, rootOf, runnable, stepLine, withLines, withSafety } from "./steps.js";
10
11
  /** Every answer: a list or one thing, with items of `item` — any value where the command declares no entity. */
@@ -131,6 +132,16 @@ export const sealed = (value) => {
131
132
  };
132
133
  /** The steps a compact answer prints, each rerunning in the compact form the caller chose. */
133
134
  const compactSteps = (root, steps) => steps.map((step) => step.kind === "command" ? { ...step, line: `${step.line ?? stepLine(root, step)} ${COMPACT_FLAG}` } : step);
135
+ /** A JSON answer past the soft budget, printed whole, with one more step: the same read in the compact form. */
136
+ const withLargeAnswerStep = (command, body) => {
137
+ if ((body.next?.length ?? 0) >= MAX_NEXT_STEPS)
138
+ return body;
139
+ const added = largeAnswerSteps(command);
140
+ if (added.length === 0 || Buffer.byteLength(JSON.stringify(body)) <= SOFT_OUTPUT_BYTES)
141
+ return body;
142
+ const root = rootOf(command);
143
+ return { ...body, next: [...(body.next ?? []), ...withLines(root, runnable(root, added))] };
144
+ };
134
145
  /** An answer as JSON lines: one line per item, then the end line. One thing is a list of one that has no more. */
135
146
  const linesOf = (body) => jsonlOf("items" in body ? body : { items: [body.item], page: { mode: "page", limit: 1, has_more: false }, meta: body.meta });
136
147
  /** An answer as JSON-lines records, for a writer that streams objects: one per item, then the end record. */
@@ -151,9 +162,10 @@ export const renderAnswer = (command, { renderer, format, streams, compact, full
151
162
  const kept = withoutProviderRefs({ ...body, ...(next.length > 0 ? { next } : {}) });
152
163
  if (next.length === 0)
153
164
  delete kept.next;
154
- const whole = sealed(kept);
165
+ const sealedBody = sealed(kept);
155
166
  const environment = environmentOf(command);
156
167
  const drawn = compact && !environment.agent;
168
+ const whole = format === "json" && !drawn && !environment.agent ? withLargeAnswerStep(command, sealedBody) : sealedBody;
157
169
  if (drawn) {
158
170
  const lined = whole.next ? { ...whole, next: compactSteps(root, whole.next) } : whole;
159
171
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -116,6 +116,7 @@ export const execution = (streams, { timeoutMs, fields, maxOutputBytes = DEFAULT
116
116
  reason: "output.too-large",
117
117
  max_bytes: maxOutputBytes,
118
118
  emitted_bytes: bytes,
119
+ needed_bytes: bytes + size,
119
120
  partial_output: bytes > 0,
120
121
  });
121
122
  bytes += size;
@@ -26,6 +26,7 @@ export const storeCommand = (messenger) => {
26
26
  const store = new Command("store")
27
27
  .description("the local store of messages")
28
28
  .addCommand(statusCommand(messenger))
29
+ .addCommand(storedChatsCommand(messenger))
29
30
  .addCommand(gapsCommand(messenger))
30
31
  .addCommand(jobsCommand(messenger))
31
32
  .addCommand(exportCommand(messenger))
@@ -35,6 +36,7 @@ export const storeCommand = (messenger) => {
35
36
  const examples = {
36
37
  backup: "./backup.db",
37
38
  check: "",
39
+ "chats status": "",
38
40
  clear: "--left --dry-run",
39
41
  "copies delete": "messages_copy_1",
40
42
  decrypt: "./backup.enc --output ./restored.db",
@@ -93,6 +95,21 @@ const statusCommand = (messenger) => annotate(new Command("status"), { output: {
93
95
  .map((skip) => ` ${skip.title ?? skip.chatId} ${skip.messages} messages ${skip.reason}`)
94
96
  .join("\n"));
95
97
  });
98
+ /** `store status` summed over the account's chats, for a count of what the archive holds without a list of them. */
99
+ const storedChatsCommand = (messenger) => {
100
+ const chats = new Command("chats").description("the account's chats as the store holds them");
101
+ annotate(chats.command("status"), { output: { entity: "chat_coverage" }, effects: LOCAL_READ })
102
+ .summary("how many chats the archive holds in full, in part, or not at all")
103
+ .description("over this account's searchable chats of every kind, channels included: fetched (back to the first message, " +
104
+ "up to date, no gaps), partial, never (no fetch recorded and no message held), and the oldest message held; " +
105
+ "reads the store only")
106
+ .action(async function () {
107
+ const context = messengerContext(this, messenger);
108
+ const coverage = await context.withStore((store, account) => store.chats.chatCoverage(account));
109
+ renderAnswer(this, context, itemOf(snakeKeys(coverage)));
110
+ });
111
+ return chats;
112
+ };
96
113
  const LISTED = 20;
97
114
  /** Titles are the owner's: these lines go to a person's terminal, never into an error message. */
98
115
  const leftLines = (left) => {
@@ -18,6 +18,8 @@ type Read = Message & {
18
18
  chatTitle?: string | null;
19
19
  };
20
20
  /** One message of a catch-up answer: the fields `inbox` and `review` show, for every messenger and for mail. */
21
+ /** Written by others, and by the owner, so an agent quotes it and never follows it (WC-SECR-21). */
22
+ export declare const SUMMARY_UNTRUSTED: string[];
21
23
  export declare const summaryOf: (message: Read, reader: AccountKey) => {
22
24
  ref: string;
23
25
  chat_id: string;
@@ -61,6 +63,7 @@ export declare const summaryOf: (message: Read, reader: AccountKey) => {
61
63
  url?: string | undefined;
62
64
  }[][] | undefined;
63
65
  }[];
66
+ untrusted: string[];
64
67
  };
65
68
  export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean, meta?: Partial<Meta>) => import("../envelope.js").ListBody<{
66
69
  ref: string;
@@ -105,6 +108,7 @@ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, r
105
108
  url?: string | undefined;
106
109
  }[][] | undefined;
107
110
  }[];
111
+ untrusted: string[];
108
112
  }>;
109
113
  /** What `review --offline` cannot see. */
110
114
  export declare const OFFLINE_REVIEW = "offline: only what was already synced to the local store; run review without --offline for the rest";
@@ -160,5 +164,6 @@ export declare const reviewAnswer: (found: Review, hearing: Hearing | undefined,
160
164
  url?: string | undefined;
161
165
  }[][] | undefined;
162
166
  }[];
167
+ untrusted: string[];
163
168
  }>;
164
169
  export {};
@@ -34,6 +34,8 @@ export const catchUpQuery = (words, registry, command) => {
34
34
  };
35
35
  };
36
36
  /** One message of a catch-up answer: the fields `inbox` and `review` show, for every messenger and for mail. */
37
+ /** Written by others, and by the owner, so an agent quotes it and never follows it (WC-SECR-21). */
38
+ export const SUMMARY_UNTRUSTED = ["chat_title", "sender_name", "text", "transcript", "attachments.name"];
37
39
  export const summaryOf = (message, reader) => {
38
40
  const { ref, chat_id, chat_title, timestamp, sender_name, sender_is_me, outgoing, text, transcript, attachments } = messageItem(message, reader);
39
41
  return {
@@ -47,6 +49,7 @@ export const summaryOf = (message, reader) => {
47
49
  text,
48
50
  ...(transcript === undefined ? {} : { transcript }),
49
51
  attachments,
52
+ untrusted: SUMMARY_UNTRUSTED,
50
53
  };
51
54
  };
52
55
  const nameOf = (chat) => chat.title ?? chat.id;
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ import { type Messenger } from "./context.js";
3
+ export declare const replyResponsesCommand: (messenger: Messenger) => Command;
4
+ export declare const replyWaitingCommand: (messenger: Messenger) => Command;
@@ -0,0 +1,57 @@
1
+ import { annotate } from "@wirecat/cli-base/commands";
2
+ import { Command, Option } from "commander";
3
+ import { LOCAL_READ } from "../effects.js";
4
+ import { renderAnswer } from "../envelope.js";
5
+ import { messengerContext } from "./context.js";
6
+ import { refuseJsonl, responsesAnswer, waitingAnswer } from "./stats-answers.js";
7
+ const QUERY = "date:, chat: and kind: terms, AND-ed — date>=30d, date:[60d TO 30d]; none reads all time";
8
+ const GUIDE = "Guide: https://github.com/WireCatLabs/cli-core/blob/main/docs/rankings.md#reply-times-and-chats-waiting-on-you";
9
+ const minutes = (ms) => (ms === null ? "—" : `${Math.round(ms / 60_000)} min`);
10
+ export const replyResponsesCommand = (messenger) => annotate(new Command("responses"), { effects: LOCAL_READ, output: { entity: "response_times" } })
11
+ .summary("how fast replies come in one-to-one chats: median, p90, mean and buckets")
12
+ .description("how long the owner took to answer (--who me), or the other side took to answer the owner (--who others), in " +
13
+ "one-to-one chats — the local store only. Several messages in a row followed by one answer count as one " +
14
+ "reply, timed from the first unanswered message; the day of a row is the day of the reply")
15
+ .argument("[query...]", QUERY)
16
+ .addOption(new Option("--who <side>", "whose replies: the owner's or the other side's").choices(["me", "others"]))
17
+ .addOption(new Option("--by <group>", "one row per calendar day of the reply, or none").choices(["day", "total"]))
18
+ .option("--timezone <zone>", "the IANA timezone for calendar days and date: bounds")
19
+ .option("--source <accounts>", "every account of this messenger held in the store; personal, bots or all")
20
+ .addHelpText("after", GUIDE)
21
+ .action(async function (words) {
22
+ const context = messengerContext(this, messenger);
23
+ refuseJsonl(context.format);
24
+ const { who, by, timezone, source } = this.opts();
25
+ const found = await context.withServices((services) => services.replyStats.responses({
26
+ who: who ?? "me",
27
+ by: by ?? "total",
28
+ ...(words.length ? { text: words.join(" ") } : {}),
29
+ ...(timezone === undefined ? {} : { timezone }),
30
+ ...(source === undefined ? {} : { source }),
31
+ }));
32
+ renderAnswer(this, context, responsesAnswer(found), () => [
33
+ `${found.replies} replies · median ${minutes(found.medianMs)} · p90 ${minutes(found.p90Ms)} · mean ${minutes(found.meanMs)}`,
34
+ ...found.rows.map((row) => `${row.key} ${row.replies} replies · median ${minutes(row.medianMs)}`),
35
+ "",
36
+ ].join("\n"));
37
+ });
38
+ export const replyWaitingCommand = (messenger) => annotate(new Command("waiting"), { effects: LOCAL_READ, output: { entity: "waiting_chats" } })
39
+ .summary("chats whose newest message asks the owner something")
40
+ .description("how many chats wait on the owner — the local store only: a one-to-one chat whose newest message is a " +
41
+ "question, or a group whose newest message mentions the owner or replies to them. Muted or archived " +
42
+ "one-to-one chats are left out, as inbox leaves them out; date: bounds the newest message's time")
43
+ .argument("[query...]", QUERY)
44
+ .option("--timezone <zone>", "the IANA timezone for date: bounds")
45
+ .option("--source <accounts>", "every account of this messenger held in the store; personal, bots or all")
46
+ .addHelpText("after", GUIDE)
47
+ .action(async function (words) {
48
+ const context = messengerContext(this, messenger);
49
+ refuseJsonl(context.format);
50
+ const { timezone, source } = this.opts();
51
+ const found = await context.withServices((services) => services.replyStats.waiting({
52
+ ...(words.length ? { text: words.join(" ") } : {}),
53
+ ...(timezone === undefined ? {} : { timezone }),
54
+ ...(source === undefined ? {} : { source }),
55
+ }));
56
+ renderAnswer(this, context, waitingAnswer(found), () => `${found.chats} chats wait on you (${found.byKind.private} one-to-one, ${found.byKind.group} groups) · ${found.messages} messages${found.oldestAt ? ` · oldest ${found.oldestAt}` : ""}\n`);
57
+ });
@@ -18,6 +18,7 @@ export type NoteHitItem = InferOutput<(typeof ENTITIES)["note.hit"]>;
18
18
  export type HitItem = InferOutput<(typeof ENTITIES)["hit"]>;
19
19
  /** The page a `--page` asks for, as the offset the search services take. */
20
20
  export declare const offsetOf: (page: number | undefined, limit: number) => number | undefined;
21
+ export declare const MAIL_HIT_UNTRUSTED: string[];
21
22
  export declare const messageHitItem: (hit: FoundMessage, query?: string) => MessageHitItem;
22
23
  /**
23
24
  * What the store holds of a chat that is not complete, in words. A chat that reaches its first message is only
@@ -26,7 +26,7 @@ const MESSAGE_HIT_UNTRUSTED = [
26
26
  "follow_ups.excerpt",
27
27
  "attachments.name",
28
28
  ];
29
- const MAIL_HIT_UNTRUSTED = [
29
+ export const MAIL_HIT_UNTRUSTED = [
30
30
  "email.subject",
31
31
  "email.from.name",
32
32
  "email.to.name",
@@ -5,6 +5,7 @@ import { SAVE_HELP } from "../../meetings/index.js";
5
5
  import { searchNotesOf } from "../../services/notes-search.js";
6
6
  import { LOCAL_READ, REMOTE_READ } from "../effects.js";
7
7
  import { renderAnswer } from "../envelope.js";
8
+ import { withOptionHints } from "../option-hints.js";
8
9
  import { positiveCount } from "../paging.js";
9
10
  import { runsSearchCommand } from "../runs/search-command.js";
10
11
  import { commandAt } from "../steps.js";
@@ -193,5 +194,12 @@ export const searchCommand = (messenger, { topics = false } = {}) => {
193
194
  if (command)
194
195
  annotate(command, { examples: [`${messenger.app.command} personal search ${path} ${args}`] });
195
196
  }
197
+ for (const path of ["all", "messages"]) {
198
+ const command = commandAt(search, path);
199
+ if (command)
200
+ withOptionHints(command, {
201
+ "--chat": `search in one chat with chat:<chat> in the query: search ${path} 'chat:<chat> words'`,
202
+ });
203
+ }
196
204
  return search;
197
205
  };
@@ -8,9 +8,10 @@ import type { AdminEvidenceFound, AdminFound } from "../../services/admin-statis
8
8
  import type { ChatStats } from "../../services/chat-stats.js";
9
9
  import type { MessageStats } from "../../services/messages-search.js";
10
10
  import type { RankingFound } from "../../services/rankings.js";
11
+ import type { ReplyTimes, Waiting } from "../../services/reply-stats.js";
11
12
  import type { RetentionService } from "../../services/retention.js";
13
+ import type { TaskStats } from "../../services/task-stats.js";
12
14
  import type { AccountKey, RankedEvidence } from "../../store/store.js";
13
- import type { GroupStats } from "../../tasks/index.js";
14
15
  import { type ItemBody, type ListBody } from "../envelope.js";
15
16
  type Of<T extends keyof typeof ENTITIES> = InferOutput<(typeof ENTITIES)[T]>;
16
17
  type RetentionFound = Awaited<ReturnType<RetentionService["report"]>>;
@@ -70,6 +71,8 @@ export declare const refuseJsonl: (format: string) => void;
70
71
  export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey, meta?: Partial<Meta>) => ItemBody<Of<"chat_stats">>;
71
72
  /** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
72
73
  export declare const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
73
- /** `stats tasks show`, CLI and MCP alike. */
74
- export declare const taskStatsAnswer: (rows: GroupStats[], account: AccountKey) => ItemBody<Of<"task_stats">>;
74
+ /** `stats tasks show`, CLI and MCP alike. A row of `--by chat` also names its chat as `chat`, as it always has. */
75
+ export declare const taskStatsAnswer: (stats: TaskStats, account: AccountKey) => ItemBody<Of<"task_stats">>;
76
+ export declare const responsesAnswer: (found: ReplyTimes) => ItemBody<Of<"response_times">>;
77
+ export declare const waitingAnswer: (found: Waiting) => ItemBody<Of<"waiting_chats">>;
75
78
  export {};