@wirecat/cli-core 0.563.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.
- package/CHANGELOG.md +77 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/messenger/archive-commands.js +17 -0
- package/dist/cli/messenger/catch-up-answers.d.ts +5 -0
- package/dist/cli/messenger/catch-up-answers.js +3 -0
- package/dist/cli/messenger/search-answers.d.ts +1 -0
- package/dist/cli/messenger/search-answers.js +1 -1
- package/dist/cli/messenger/stats-answers.d.ts +3 -3
- package/dist/cli/messenger/stats-answers.js +21 -9
- package/dist/cli/messenger/tasks-command.js +35 -14
- package/dist/conversations/mail-text.d.ts +6 -0
- package/dist/conversations/mail-text.js +73 -0
- package/dist/domain/calendar.d.ts +2 -0
- package/dist/domain/calendar.js +6 -0
- package/dist/entities/chat-stats.d.ts +16 -2
- package/dist/entities/chat-stats.js +18 -3
- package/dist/entities/index.d.ts +50 -802
- package/dist/entities/index.js +23 -16
- package/dist/entities/mail-counts.d.ts +15 -0
- package/dist/entities/mail-counts.js +16 -0
- package/dist/entities/meeting.d.ts +1 -1
- package/dist/entities/meeting.js +1 -1
- package/dist/entities/store-records.d.ts +8 -0
- package/dist/entities/store-records.js +8 -0
- package/dist/errors/reasons.js +5 -0
- package/dist/mail/cli/index.d.ts +5 -0
- package/dist/mail/cli/index.js +27 -3
- package/dist/mail/cli/review.js +11 -4
- package/dist/mail/index.d.ts +2 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/stats.d.ts +50 -0
- package/dist/mail/stats.js +60 -0
- package/dist/mail/whole.d.ts +4 -4
- package/dist/mail/whole.js +4 -3
- package/dist/meetings/index.d.ts +2 -2
- package/dist/meetings/index.js +2 -2
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/meetings/read-answers.d.ts +22 -1
- package/dist/meetings/read-answers.js +38 -0
- package/dist/meetings/stats.d.ts +44 -0
- package/dist/meetings/stats.js +77 -7
- package/dist/search/lucene/registry.js +2 -1
- package/dist/search/question-plan.d.ts +16 -0
- package/dist/search/question-plan.js +34 -0
- package/dist/services/events-search.d.ts +5 -1
- package/dist/services/events-search.js +10 -4
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/meeting-stats.d.ts +12 -1
- package/dist/services/meeting-stats.js +12 -0
- package/dist/services/messages-discovery.d.ts +1 -5
- package/dist/services/messages-discovery.js +5 -18
- package/dist/services/messages-meaning.js +25 -3
- package/dist/services/messages.js +4 -3
- package/dist/services/notes-search.d.ts +5 -1
- package/dist/services/notes-search.js +16 -5
- package/dist/services/proposals/ids.d.ts +2 -2
- package/dist/services/proposals/ids.js +5 -3
- package/dist/services/proposals/kinds/findings.d.ts +1 -1
- package/dist/services/proposals/kinds/findings.js +1 -1
- package/dist/services/proposals/kinds/meeting-findings.d.ts +7 -0
- package/dist/services/proposals/kinds/meeting-findings.js +73 -0
- package/dist/services/proposals/kinds/task-close.d.ts +7 -0
- package/dist/services/proposals/kinds/task-close.js +76 -0
- package/dist/services/proposals/registry.js +4 -0
- package/dist/services/search-all.d.ts +5 -0
- package/dist/services/search-all.js +47 -14
- package/dist/services/task-stats.d.ts +41 -0
- package/dist/services/task-stats.js +126 -0
- package/dist/services/tasks.d.ts +17 -4
- package/dist/services/tasks.js +31 -14
- package/dist/store/chats.d.ts +3 -1
- package/dist/store/chats.js +6 -0
- package/dist/store/decisions.d.ts +4 -2
- package/dist/store/decisions.js +5 -3
- package/dist/store/exports.d.ts +1 -1
- package/dist/store/exports.js +1 -1
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.js +1 -1
- package/dist/store/sqlite/completeness.d.ts +17 -0
- package/dist/store/sqlite/completeness.js +49 -13
- package/dist/store/sqlite/lucene-fields.js +2 -0
- package/dist/store/sqlite/lucene-groups.d.ts +15 -0
- package/dist/store/sqlite/lucene-groups.js +21 -0
- package/dist/store/sqlite/lucene.d.ts +2 -9
- package/dist/store/sqlite/lucene.js +1 -12
- package/dist/store/sqlite/meeting-stats.d.ts +2 -0
- package/dist/store/sqlite/meeting-stats.js +27 -1
- package/dist/store/sqlite/retention.js +2 -7
- package/dist/store/sqlite/task-board.js +2 -0
- package/dist/store/sqlite/task-stats.d.ts +32 -0
- package/dist/store/sqlite/task-stats.js +45 -0
- package/dist/store/sqlite/tasks.d.ts +2 -1
- package/dist/store/sqlite/tasks.js +2 -0
- package/package.json +1 -1
- package/parity.json +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,83 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
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
|
+
|
|
10
87
|
## 0.563.0 — 03.11.2026
|
|
11
88
|
|
|
12
89
|
### Changed — may break callers
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -10,8 +10,8 @@ import type { MessageStats } from "../../services/messages-search.js";
|
|
|
10
10
|
import type { RankingFound } from "../../services/rankings.js";
|
|
11
11
|
import type { ReplyTimes, Waiting } from "../../services/reply-stats.js";
|
|
12
12
|
import type { RetentionService } from "../../services/retention.js";
|
|
13
|
+
import type { TaskStats } from "../../services/task-stats.js";
|
|
13
14
|
import type { AccountKey, RankedEvidence } from "../../store/store.js";
|
|
14
|
-
import type { GroupStats } from "../../tasks/index.js";
|
|
15
15
|
import { type ItemBody, type ListBody } from "../envelope.js";
|
|
16
16
|
type Of<T extends keyof typeof ENTITIES> = InferOutput<(typeof ENTITIES)[T]>;
|
|
17
17
|
type RetentionFound = Awaited<ReturnType<RetentionService["report"]>>;
|
|
@@ -71,8 +71,8 @@ export declare const refuseJsonl: (format: string) => void;
|
|
|
71
71
|
export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey, meta?: Partial<Meta>) => ItemBody<Of<"chat_stats">>;
|
|
72
72
|
/** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
|
|
73
73
|
export declare const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
|
|
74
|
-
/** `stats tasks show`, CLI and MCP alike. */
|
|
75
|
-
export declare const taskStatsAnswer: (
|
|
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
76
|
export declare const responsesAnswer: (found: ReplyTimes) => ItemBody<Of<"response_times">>;
|
|
77
77
|
export declare const waitingAnswer: (found: Waiting) => ItemBody<Of<"waiting_chats">>;
|
|
78
78
|
export {};
|
|
@@ -410,15 +410,27 @@ export const chatStatsAnswer = (stats, account, meta) => {
|
|
|
410
410
|
};
|
|
411
411
|
/** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
|
|
412
412
|
export const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
|
|
413
|
-
/** `stats tasks show`, CLI and MCP alike. */
|
|
414
|
-
export const taskStatsAnswer = (
|
|
415
|
-
open:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
413
|
+
/** `stats tasks show`, CLI and MCP alike. A row of `--by chat` also names its chat as `chat`, as it always has. */
|
|
414
|
+
export const taskStatsAnswer = (stats, account) => itemOf({
|
|
415
|
+
open: stats.open,
|
|
416
|
+
created: stats.created,
|
|
417
|
+
closed: stats.closed,
|
|
418
|
+
median_close_ms: stats.medianCloseMs ?? null,
|
|
419
|
+
mean_open_age_ms: stats.meanOpenAgeMs ?? null,
|
|
420
|
+
rows: stats.rows.map((row) => {
|
|
421
|
+
const chat = stats.by === "chat" ? chatRef(account, row.group) : undefined;
|
|
422
|
+
return {
|
|
423
|
+
...(chat === undefined ? {} : { chat }),
|
|
424
|
+
key: chat ?? (stats.by === "project" ? `project:${row.group}` : row.group),
|
|
425
|
+
name: row.name,
|
|
426
|
+
open: row.open,
|
|
427
|
+
created: row.created,
|
|
428
|
+
closed: row.closed,
|
|
429
|
+
oldest_open_at: row.oldestOpenAt?.toISOString() ?? null,
|
|
430
|
+
median_close_ms: row.medianCloseMs ?? null,
|
|
431
|
+
mean_open_age_ms: row.meanOpenAgeMs ?? null,
|
|
432
|
+
};
|
|
433
|
+
}),
|
|
422
434
|
});
|
|
423
435
|
const replyFigures = (one) => ({
|
|
424
436
|
replies: one.replies,
|
|
@@ -8,7 +8,8 @@ import { aheadOf, momentOf } from "../../services/moment.js";
|
|
|
8
8
|
import { refuseAlreadyRead } from "../../services/task-analysis.js";
|
|
9
9
|
import { unlistedNote } from "../../services/task-categories.js";
|
|
10
10
|
import { ARRIVAL_WINDOW, applyTaskRulesToChats } from "../../services/task-rules.js";
|
|
11
|
-
import {
|
|
11
|
+
import { TASK_STATS_BY } from "../../services/task-stats.js";
|
|
12
|
+
import { CLAIM_LEASE_MS, closedStateOf, TASK_PAIR_QUERY, TASK_QUERY, TASK_STATS_QUERY, taskLinkKindOf, taskTypeOf, } from "../../services/tasks.js";
|
|
12
13
|
import { PRIORITY_LABELS, priorityOption } from "../../tasks/index.js";
|
|
13
14
|
import { actorOf, holderOf as holderIn } from "../author.js";
|
|
14
15
|
import { environmentOf } from "../context.js";
|
|
@@ -539,32 +540,52 @@ export const tasksCommand = (messenger) => {
|
|
|
539
540
|
});
|
|
540
541
|
return tasks;
|
|
541
542
|
};
|
|
543
|
+
const statsByOf = (value) => {
|
|
544
|
+
if (!TASK_STATS_BY.includes(value))
|
|
545
|
+
throw new CliError("validation_error", `--by takes ${TASK_STATS_BY.join(", ")}`);
|
|
546
|
+
return value;
|
|
547
|
+
};
|
|
548
|
+
const minutes = (ms) => (ms === undefined ? "" : `${Math.round(ms / 60_000)} min`);
|
|
542
549
|
export const tasksStatsCommand = (messenger) => {
|
|
543
|
-
return annotate(new Command("show"), {
|
|
544
|
-
|
|
550
|
+
return withQuery(annotate(new Command("show"), {
|
|
551
|
+
effects: LOCAL_READ,
|
|
552
|
+
output: { entity: "task_stats" },
|
|
553
|
+
examples: [`${messenger.app.command} stats tasks show --by meeting -- created>=30d`],
|
|
554
|
+
}), TASK_STATS_QUERY)
|
|
555
|
+
.summary("count tasks by chat, meeting or project, open and closed")
|
|
556
|
+
.description("tasks per chat, meeting or project: open now, and opened and closed in the period (created: in the query), " +
|
|
557
|
+
"with the median time to close and the mean age of the open ones")
|
|
558
|
+
.option("--by <group>", "chat (default) or meeting: this account's tasks; project: the owner's project tasks, every project", statsByOf)
|
|
545
559
|
.option("--chat <chat>", `only this chat; ${messenger.chatArgument}`)
|
|
546
560
|
.option("--type <type>", "only this type: question, request, mention or promise", taskTypeOf)
|
|
547
|
-
.
|
|
561
|
+
.option("--timezone <zone>", "the IANA timezone for calendar days in the period, such as created>=today")
|
|
562
|
+
.action(async function (words) {
|
|
548
563
|
const context = messengerContext(this, messenger);
|
|
549
|
-
const { chat, type } = this.opts();
|
|
564
|
+
const { by, chat, type, timezone } = this.opts();
|
|
550
565
|
refuseJsonl(context.format);
|
|
551
|
-
const
|
|
552
|
-
|
|
566
|
+
const query = listQuery(typedQuery(words), TASK_STATS_QUERY, "stats tasks show");
|
|
567
|
+
const { stats, account } = await context.withServices(async (services) => ({
|
|
568
|
+
stats: await services.tasks.stats({
|
|
569
|
+
...(by === undefined ? {} : { by }),
|
|
553
570
|
...(chat === undefined ? {} : { chat }),
|
|
554
|
-
...(type === undefined ? {} : { type }),
|
|
571
|
+
...(type === undefined ? {} : { kind: type }),
|
|
572
|
+
...(query === undefined ? {} : { query }),
|
|
573
|
+
...(timezone === undefined ? {} : { timezone }),
|
|
555
574
|
}),
|
|
556
575
|
account: await services.messages.reader(),
|
|
557
576
|
}));
|
|
558
|
-
if (rows.length === 0)
|
|
577
|
+
if (stats.rows.length === 0)
|
|
559
578
|
context.renderer.note("no tasks yet");
|
|
560
|
-
renderAnswer(this, context, taskStatsAnswer(
|
|
579
|
+
renderAnswer(this, context, taskStatsAnswer(stats, account), () => stats.rows.length === 0
|
|
561
580
|
? ""
|
|
562
|
-
: `${rows
|
|
563
|
-
.map(({ group, open, oldestOpenAt, medianCloseMs }) => [
|
|
564
|
-
group,
|
|
581
|
+
: `${stats.rows
|
|
582
|
+
.map(({ group, name, open, created, closed, oldestOpenAt, medianCloseMs }) => [
|
|
583
|
+
name ? `${name} (${group})` : group,
|
|
565
584
|
`${open} open`,
|
|
585
|
+
`${created} created`,
|
|
586
|
+
`${closed} closed`,
|
|
566
587
|
oldestOpenAt ? `oldest ${oldestOpenAt.toISOString().slice(0, 16).replace("T", " ")}` : "",
|
|
567
|
-
medianCloseMs === undefined ? "" : `median ${
|
|
588
|
+
medianCloseMs === undefined ? "" : `median ${minutes(medianCloseMs)} to close`,
|
|
568
589
|
]
|
|
569
590
|
.filter(Boolean)
|
|
570
591
|
.join(" "))
|
|
@@ -4,3 +4,9 @@
|
|
|
4
4
|
* not hold stays, so a forward or a reply to an unsaved email is still found.
|
|
5
5
|
*/
|
|
6
6
|
export declare const mailCore: (body: string, earlier: string[]) => string;
|
|
7
|
+
/**
|
|
8
|
+
* What an email says itself, for a model that should not read earlier mail or the sender's signature: no quoted
|
|
9
|
+
* lines, nothing after a reply or Outlook header, no signature or mobile footer. A forwarded message is kept whole,
|
|
10
|
+
* since it is what was sent. When nothing would be left — an email that is only a quote — the body stays as it is.
|
|
11
|
+
*/
|
|
12
|
+
export declare const mailOwnText: (body: string) => string;
|
|
@@ -86,3 +86,76 @@ export const mailCore = (body, earlier) => {
|
|
|
86
86
|
const kept = trimEdges(lines.filter((_, i) => !drop.has(i)));
|
|
87
87
|
return withoutSignOff(withoutGreeting(kept)).join("\n").trim();
|
|
88
88
|
};
|
|
89
|
+
const QUOTED = /^\s*>/;
|
|
90
|
+
const ATTRIBUTION_END = /(wrote|написал(\(а\)|а|и)?|escribió|a écrit|schrieb\s.+|<[^<>\s]+@[^<>\s]+>)\s*:\s*$/iu;
|
|
91
|
+
/** The first line of an attribution a client wrapped in two: "On Mon, 3 Nov 2026 at 10:00, Alice Example <". */
|
|
92
|
+
const ATTRIBUTION_START = /^(on|le|am|el|в)\s/iu;
|
|
93
|
+
const DATED = /^(on|le|am|el|в|\d)/iu;
|
|
94
|
+
const ORIGINAL = /^-{2,}\s*(original message|ursprüngliche nachricht|message d'origine|mensaje original|исходное сообщение)\s*-{2,}$/iu;
|
|
95
|
+
const FROM_LINE = /^(from|von|de|от)\s?:\s*\S/iu;
|
|
96
|
+
const DATE_LINE = /^(sent|date|gesendet|datum|envoyé|enviado|fecha|отправлено|дата)\s?:/iu;
|
|
97
|
+
const FORWARD = /^(-{2,}\s*(forwarded message|weitergeleitete nachricht|message transféré|mensaje reenviado|пересылаемое сообщение)\s*-{2,}|begin forwarded message:)$/iu;
|
|
98
|
+
const RULE = /^_{5,}$/;
|
|
99
|
+
const MOBILE = /^(sent from my |von meinem .+ gesendet$|envoyé de mon |отправлено с |enviado desde mi )/iu;
|
|
100
|
+
/** Where an attribution ending at `i` begins, or -1 when the line is not one. */
|
|
101
|
+
const attributionAt = (lines, i) => {
|
|
102
|
+
const line = (lines[i] ?? "").trim();
|
|
103
|
+
if (!ATTRIBUTION_END.test(line))
|
|
104
|
+
return -1;
|
|
105
|
+
const prior = (lines[i - 1] ?? "").trim();
|
|
106
|
+
return prior && ATTRIBUTION_START.test(prior) && !ATTRIBUTION_START.test(line) ? i - 1 : i;
|
|
107
|
+
};
|
|
108
|
+
/** An Outlook header — From, then Sent or Date within the next lines — or its "Original Message" divider. */
|
|
109
|
+
const outlookAt = (lines, i) => {
|
|
110
|
+
const line = (lines[i] ?? "").trim();
|
|
111
|
+
if (ORIGINAL.test(line))
|
|
112
|
+
return true;
|
|
113
|
+
return FROM_LINE.test(line) && lines.slice(i + 1, i + 4).some((next) => DATE_LINE.test(next.trim()));
|
|
114
|
+
};
|
|
115
|
+
const nextText = (lines, from) => lines.slice(from).find((line) => line.trim()) ?? "";
|
|
116
|
+
/** The lines an email adds, in reading order: quotes dropped, and everything after a reply header cut. */
|
|
117
|
+
const ownLines = (lines) => {
|
|
118
|
+
const kept = [];
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
const line = lines[i] ?? "";
|
|
121
|
+
if (QUOTED.test(line))
|
|
122
|
+
continue;
|
|
123
|
+
if (outlookAt(lines, i)) {
|
|
124
|
+
const last = kept.findLastIndex((one) => one.trim());
|
|
125
|
+
return last !== -1 && RULE.test((kept[last] ?? "").trim()) ? kept.slice(0, last) : kept;
|
|
126
|
+
}
|
|
127
|
+
const start = attributionAt(lines, i);
|
|
128
|
+
if (start === -1) {
|
|
129
|
+
kept.push(line);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (start < i)
|
|
133
|
+
kept.pop();
|
|
134
|
+
if (QUOTED.test(nextText(lines, i + 1)))
|
|
135
|
+
continue;
|
|
136
|
+
// Without quote marks only a dated header is one: "Here is what Bob wrote:" is not.
|
|
137
|
+
const header = lines
|
|
138
|
+
.slice(start, i + 1)
|
|
139
|
+
.join(" ")
|
|
140
|
+
.trim();
|
|
141
|
+
if (/\d/.test(header) && (header.includes("@") || DATED.test(header)) && header.length <= 300)
|
|
142
|
+
return kept;
|
|
143
|
+
if (start < i)
|
|
144
|
+
kept.push(lines[start] ?? "");
|
|
145
|
+
kept.push(line);
|
|
146
|
+
}
|
|
147
|
+
return kept;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* What an email says itself, for a model that should not read earlier mail or the sender's signature: no quoted
|
|
151
|
+
* lines, nothing after a reply or Outlook header, no signature or mobile footer. A forwarded message is kept whole,
|
|
152
|
+
* since it is what was sent. When nothing would be left — an email that is only a quote — the body stays as it is.
|
|
153
|
+
*/
|
|
154
|
+
export const mailOwnText = (body) => {
|
|
155
|
+
const lines = body.replace(/\r\n?/g, "\n").split("\n");
|
|
156
|
+
const forward = lines.findIndex((line) => FORWARD.test(line.trim()));
|
|
157
|
+
const head = forward === -1 ? lines : lines.slice(0, forward);
|
|
158
|
+
const own = withoutSignOff(trimEdges(ownLines(head).filter((line) => !MOBILE.test(line.trim()))));
|
|
159
|
+
const text = [...own, ...(forward === -1 ? [] : ["", ...lines.slice(forward)])].join("\n").trim();
|
|
160
|
+
return text || body.trim();
|
|
161
|
+
};
|
package/dist/domain/calendar.js
CHANGED
|
@@ -11,3 +11,9 @@ export const calendarKey = (zone, by) => {
|
|
|
11
11
|
return `${part.year}-${part.month}-${part.day}${by === "hour" ? `T${part.hour}` : ""}`;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
+
/** The Monday that starts the week of a `YYYY-MM-DD` day. */
|
|
15
|
+
export const weekKey = (day) => {
|
|
16
|
+
const date = new Date(`${day}T00:00:00Z`);
|
|
17
|
+
date.setUTCDate(date.getUTCDate() - ((date.getUTCDay() + 6) % 7));
|
|
18
|
+
return date.toISOString().slice(0, 10);
|
|
19
|
+
};
|
|
@@ -62,14 +62,28 @@ export declare const chatStats: v.StrictObjectSchema<{
|
|
|
62
62
|
readonly left: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
63
63
|
}, undefined>, undefined>, undefined>;
|
|
64
64
|
}, undefined>;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Open and closed tasks, one row per chat, meeting or project. `open` is open now; `created` and `closed` count the
|
|
67
|
+
* tasks opened and closed in the query's period, every task without one.
|
|
68
|
+
*/
|
|
66
69
|
export declare const taskStats: v.StrictObjectSchema<{
|
|
67
70
|
/** Open tasks across every row. */
|
|
68
71
|
readonly open: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
72
|
+
readonly created: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
73
|
+
readonly closed: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
74
|
+
readonly median_close_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
75
|
+
readonly mean_open_age_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
69
76
|
readonly rows: v.ArraySchema<v.StrictObjectSchema<{
|
|
70
|
-
|
|
77
|
+
/** Rows of `--by chat` only. */
|
|
78
|
+
readonly chat: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
79
|
+
/** The chat reference, `meeting:<account>/<meeting>` or `project:<id>`. */
|
|
80
|
+
readonly key: v.StringSchema<undefined>;
|
|
81
|
+
readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
71
82
|
readonly open: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
83
|
+
readonly created: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
84
|
+
readonly closed: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
72
85
|
readonly oldest_open_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>;
|
|
73
86
|
readonly median_close_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
87
|
+
readonly mean_open_age_ms: v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>;
|
|
74
88
|
}, undefined>, undefined>;
|
|
75
89
|
}, undefined>;
|
|
@@ -54,14 +54,29 @@ export const chatStats = v.strictObject({
|
|
|
54
54
|
})),
|
|
55
55
|
series: v.optional(v.array(v.strictObject({ key: text, messages: count, senders: count, joined: optionalCount, left: optionalCount }))),
|
|
56
56
|
});
|
|
57
|
-
|
|
57
|
+
const ms = v.nullable(v.pipe(v.number(), v.minValue(0)));
|
|
58
|
+
/**
|
|
59
|
+
* Open and closed tasks, one row per chat, meeting or project. `open` is open now; `created` and `closed` count the
|
|
60
|
+
* tasks opened and closed in the query's period, every task without one.
|
|
61
|
+
*/
|
|
58
62
|
export const taskStats = v.strictObject({
|
|
59
63
|
/** Open tasks across every row. */
|
|
60
64
|
open: count,
|
|
65
|
+
created: count,
|
|
66
|
+
closed: count,
|
|
67
|
+
median_close_ms: ms,
|
|
68
|
+
mean_open_age_ms: ms,
|
|
61
69
|
rows: v.array(v.strictObject({
|
|
62
|
-
chat
|
|
70
|
+
/** Rows of `--by chat` only. */
|
|
71
|
+
chat: v.optional(reference),
|
|
72
|
+
/** The chat reference, `meeting:<account>/<meeting>` or `project:<id>`. */
|
|
73
|
+
key: text,
|
|
74
|
+
name: v.nullable(text),
|
|
63
75
|
open: count,
|
|
76
|
+
created: count,
|
|
77
|
+
closed: count,
|
|
64
78
|
oldest_open_at: v.nullable(time),
|
|
65
|
-
median_close_ms:
|
|
79
|
+
median_close_ms: ms,
|
|
80
|
+
mean_open_age_ms: ms,
|
|
66
81
|
})),
|
|
67
82
|
});
|