@wirecat/cli-core 0.552.0 → 0.554.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 +36 -0
- package/dist/cli/compact.d.ts +4 -0
- package/dist/cli/compact.js +65 -7
- package/dist/cli/messenger/context.d.ts +3 -0
- package/dist/cli/messenger/projects-command.js +38 -11
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/project-evidence.d.ts +33 -4
- package/dist/cli/project-evidence.js +38 -4
- package/dist/cli/project-status-rows.js +11 -1
- package/dist/cli/project-status.d.ts +24 -0
- package/dist/cli/project-status.js +52 -9
- package/dist/cli/schedule/schedule-command.d.ts +0 -1
- package/dist/cli/schedule/schedule-command.js +6 -1
- package/dist/mail/email-input.d.ts +2 -0
- package/dist/mail/email-input.js +6 -1
- package/dist/mail/retry.js +2 -1
- package/dist/mail/sync.js +5 -1
- package/dist/mail/testing/fake.d.ts +2 -0
- package/dist/mail/testing/fake.js +1 -1
- package/dist/services/index.d.ts +4 -3
- package/dist/services/index.js +4 -3
- package/dist/services/link-scorers/index.d.ts +1 -1
- package/dist/services/link-scorers/index.js +1 -1
- package/dist/services/link-scorers/scan.d.ts +0 -1
- package/dist/services/link-scorers/scan.js +1 -1
- package/dist/services/project-purposes.d.ts +28 -0
- package/dist/services/project-purposes.js +41 -0
- package/dist/services/project-status/index.d.ts +22 -8
- package/dist/services/project-status/index.js +17 -9
- package/dist/services/stored-project-gathering.d.ts +79 -4
- package/dist/services/stored-project-gathering.js +171 -22
- package/dist/store/messages.d.ts +2 -1
- package/dist/store/messages.js +2 -2
- package/dist/store/sqlite/email-locations.d.ts +1 -0
- package/dist/store/sqlite/email-locations.js +4 -0
- package/dist/store/sqlite/emails.d.ts +2 -0
- package/dist/store/sqlite/emails.js +3 -6
- package/dist/store/sqlite/reads.d.ts +1 -1
- package/dist/store/sqlite/reads.js +2 -2
- package/package.json +1 -1
- package/parity.json +10 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ContextSource } from "../context/evidence.js";
|
|
2
|
-
import type { MessageStore } from "../store/store.js";
|
|
2
|
+
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
3
3
|
import { gatherProjectEvidence, type ProjectChatFetcher, type ProjectGatheringPorts } from "./project-gathering.js";
|
|
4
4
|
import { type EvidenceMentions } from "./project-gathering-mentions.js";
|
|
5
|
+
import { type PurposeConfig } from "./project-purposes.js";
|
|
5
6
|
export interface StoredProjectInput {
|
|
6
7
|
project: string;
|
|
7
8
|
/** The sources the host resolved from its `--source`, and those it could not read. */
|
|
@@ -31,19 +32,81 @@ export interface StoredProjectInput {
|
|
|
31
32
|
maxFetchMs?: number;
|
|
32
33
|
onFetchProgress?: ProjectGatheringPorts["onFetchProgress"];
|
|
33
34
|
signal?: AbortSignal;
|
|
35
|
+
/** The linked chats' purposes to read; `["all"]` reads every one. Left out, the config's defaults. */
|
|
36
|
+
purposes?: readonly string[];
|
|
37
|
+
/** The host's purposes; core's built-in ones when left out. */
|
|
38
|
+
purposeConfig?: PurposeConfig;
|
|
39
|
+
/** Proposed `member-of` links count as linked chats and members too, after the confirmed ones, every item marked. */
|
|
40
|
+
includeProposed?: boolean;
|
|
41
|
+
/** How many messages a left-out chat holds in the window; the store's own count when left out. */
|
|
42
|
+
count?: ChatCounter;
|
|
34
43
|
}
|
|
44
|
+
export interface ChatCount {
|
|
45
|
+
messages: number;
|
|
46
|
+
firstAt: string | null;
|
|
47
|
+
lastAt: string | null;
|
|
48
|
+
}
|
|
49
|
+
export type ChatCounter = (account: AccountKey, chatId: string, window: {
|
|
50
|
+
since: number;
|
|
51
|
+
until: number;
|
|
52
|
+
}) => Promise<ChatCount>;
|
|
53
|
+
/** The store's count of one chat's messages in a window, `until` exclusive, and the first and last of them. */
|
|
54
|
+
export declare const storedChatCount: (store: MessageStore) => ChatCounter;
|
|
35
55
|
/** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
|
|
36
56
|
export declare const canonicalProjectRef: (store: MessageStore, reference: string) => Promise<string>;
|
|
57
|
+
export interface ProjectMember {
|
|
58
|
+
person: string;
|
|
59
|
+
state: "read" | "member-limit" | "no-identity";
|
|
60
|
+
/** The proposal that names this person a member; read only with `includeProposed`. */
|
|
61
|
+
proposed?: string;
|
|
62
|
+
score?: number;
|
|
63
|
+
}
|
|
37
64
|
export interface LinkedChat {
|
|
38
65
|
provider: string;
|
|
39
66
|
account: string;
|
|
40
67
|
chat: string;
|
|
41
68
|
/** The link's free role (`source report`, `what worked`), if the owner set one. */
|
|
42
69
|
role: string | null;
|
|
70
|
+
/** What the chat is for in the project; `null` means `work`. */
|
|
71
|
+
purpose: string | null;
|
|
72
|
+
/** The confirmed link's id; null for a proposed one. */
|
|
73
|
+
relationship: string | null;
|
|
74
|
+
/** The proposal that links the chat; only with `includeProposed`. */
|
|
75
|
+
proposed?: string;
|
|
76
|
+
score?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Links the owner confirmed, then, when asked, the proposed ones: a suggestion would put a whole chat into the
|
|
80
|
+
* evidence on a guess, so it is read only on request and marked.
|
|
81
|
+
*/
|
|
82
|
+
export declare const linkedChatsOf: (store: MessageStore, project: string | null, options?: {
|
|
83
|
+
includeProposed?: boolean;
|
|
84
|
+
}) => Promise<LinkedChat[]>;
|
|
85
|
+
/** A linked chat whose purpose this run did not read: counted, never listed. */
|
|
86
|
+
export interface LeftOutChat {
|
|
87
|
+
ref: string;
|
|
88
|
+
provider: string;
|
|
89
|
+
account: string;
|
|
90
|
+
chat: string;
|
|
91
|
+
title: string | null;
|
|
92
|
+
purpose: string;
|
|
93
|
+
relationship: string | null;
|
|
94
|
+
proposed?: string;
|
|
95
|
+
messages: number;
|
|
96
|
+
firstAt: string | null;
|
|
97
|
+
lastAt: string | null;
|
|
43
98
|
}
|
|
44
|
-
|
|
45
|
-
export
|
|
46
|
-
|
|
99
|
+
type Gathered = Awaited<ReturnType<typeof gatherProjectEvidence>>;
|
|
100
|
+
export type StoredProjectGathering = Omit<Gathered, "items" | "linkedChats"> & {
|
|
101
|
+
/** `proposed`: the proposal whose link alone brought the item in; only with `includeProposed`. */
|
|
102
|
+
items: (Gathered["items"][number] & {
|
|
103
|
+
proposed?: string;
|
|
104
|
+
})[];
|
|
105
|
+
linkedChats: (Gathered["linkedChats"][number] & {
|
|
106
|
+
purpose: string;
|
|
107
|
+
proposed?: string;
|
|
108
|
+
score?: number;
|
|
109
|
+
})[];
|
|
47
110
|
projectRef: string | null;
|
|
48
111
|
unreadSources: {
|
|
49
112
|
source: string;
|
|
@@ -51,9 +114,21 @@ export type StoredProjectGathering = Awaited<ReturnType<typeof gatherProjectEvid
|
|
|
51
114
|
reason?: "out-of-scope" | "linked-chat-limit" | "member-limit" | "no-identity" | "unknown-provider";
|
|
52
115
|
}[];
|
|
53
116
|
mentions: EvidenceMentions;
|
|
117
|
+
/** The purposes read: `all`, or the list. */
|
|
118
|
+
purposes: "all" | string[];
|
|
119
|
+
leftOut: LeftOutChat[];
|
|
120
|
+
members: ProjectMember[];
|
|
121
|
+
/** One linked chat with no purpose set gave most of this run's items; a first run only, so it is said once. */
|
|
122
|
+
purposeHint: {
|
|
123
|
+
chat: string;
|
|
124
|
+
relationship: string;
|
|
125
|
+
items: number;
|
|
126
|
+
of: number;
|
|
127
|
+
} | null;
|
|
54
128
|
};
|
|
55
129
|
/**
|
|
56
130
|
* `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
|
|
57
131
|
* confirmed person members and its confirmed linked chats, in the sources the host resolved.
|
|
58
132
|
*/
|
|
59
133
|
export declare const gatherStoredProject: (store: MessageStore, input: StoredProjectInput) => Promise<StoredProjectGathering>;
|
|
134
|
+
export {};
|
|
@@ -4,6 +4,21 @@ import { chatCoverage } from "./chat-coverage.js";
|
|
|
4
4
|
import { gatherProjectEvidence } from "./project-gathering.js";
|
|
5
5
|
import { storedNeighbours } from "./project-gathering-around.js";
|
|
6
6
|
import { findEvidenceMentions } from "./project-gathering-mentions.js";
|
|
7
|
+
import { LINK_PURPOSE, purposeRead, purposesOf } from "./project-purposes.js";
|
|
8
|
+
/** The store's count of one chat's messages in a window, `until` exclusive, and the first and last of them. */
|
|
9
|
+
export const storedChatCount = (store) => async (account, chatId, { since, until }) => {
|
|
10
|
+
const window = { since: new Date(since).toISOString(), until: new Date(until - 1).toISOString() };
|
|
11
|
+
const [messages, newest, oldest] = await Promise.all([
|
|
12
|
+
store.messages.countMessages(account, chatId, window),
|
|
13
|
+
store.messages.messages(account, chatId, { limit: 1, since: window.since, until: until - 1 }),
|
|
14
|
+
store.messages.messages(account, chatId, { limit: 1, after: { time: since - 1 }, until: until - 1 }),
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
messages,
|
|
18
|
+
firstAt: oldest.items[0]?.timestamp ?? null,
|
|
19
|
+
lastAt: newest.items.at(-1)?.timestamp ?? null,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
7
22
|
const PROJECT_ID = /^project:[1-9]\d*$/;
|
|
8
23
|
/** `project:<id>`, or the one project a key, name or alias names; several or none is refused, never guessed. */
|
|
9
24
|
export const canonicalProjectRef = async (store, reference) => {
|
|
@@ -85,59 +100,122 @@ const contactOf = async (store, contact) => {
|
|
|
85
100
|
};
|
|
86
101
|
const LINKED_CHATS = 16;
|
|
87
102
|
const CORE_LIMIT = 16;
|
|
88
|
-
/**
|
|
89
|
-
const
|
|
103
|
+
/** Pending `member-of` proposals to the project, oldest first, so a cut at a limit always drops the same ones. */
|
|
104
|
+
const proposedLinksOf = async (store, project) => {
|
|
105
|
+
const stored = await store.proposedActions.list({ kinds: ["relationship"], status: "proposed", project, limit: 500 });
|
|
106
|
+
const proposals = stored
|
|
107
|
+
.flatMap((action) => {
|
|
108
|
+
const payload = action.payload;
|
|
109
|
+
const { purpose, score } = action.payload;
|
|
110
|
+
if (payload?.kind !== "member-of" || payload.to !== project || typeof payload.from !== "string")
|
|
111
|
+
return [];
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
from: payload.from,
|
|
115
|
+
id: action.id,
|
|
116
|
+
role: typeof payload.role === "string" ? payload.role : null,
|
|
117
|
+
purpose: typeof purpose === "string" ? purpose : null,
|
|
118
|
+
...(typeof score === "number" ? { score } : {}),
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
})
|
|
122
|
+
.sort((a, b) => Number(a.id) - Number(b.id));
|
|
123
|
+
const legacy = (await store.knowledge.relations(null, project))
|
|
124
|
+
.filter((one) => !one.confirmed && one.kind === "member-of" && one.to === project)
|
|
125
|
+
.map((one) => ({ from: one.from, id: `link:${one.id}`, role: one.role, purpose: one.purpose }));
|
|
126
|
+
return [...proposals, ...legacy];
|
|
127
|
+
};
|
|
128
|
+
/** Confirmed `person` members in a stable order, then proposed ones when asked, so a cut drops the same ones. */
|
|
129
|
+
const memberRefsOf = async (store, project, includeProposed) => {
|
|
90
130
|
if (!project)
|
|
91
131
|
return [];
|
|
92
|
-
const
|
|
132
|
+
const confirmed = new Set();
|
|
93
133
|
for (const relation of await store.knowledge.relations(null, project))
|
|
94
134
|
if (relation.confirmed &&
|
|
95
135
|
relation.kind === "member-of" &&
|
|
96
136
|
relation.to === project &&
|
|
97
137
|
/^person:.+/.test(relation.from))
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
confirmed.add(relation.from);
|
|
139
|
+
const members = [...confirmed].sort().map((ref) => ({ ref }));
|
|
140
|
+
if (!includeProposed)
|
|
141
|
+
return members;
|
|
142
|
+
for (const one of await proposedLinksOf(store, project))
|
|
143
|
+
if (/^person:.+/.test(one.from) && !members.some((member) => member.ref === one.from))
|
|
144
|
+
members.push({ ref: one.from, proposed: one.id, ...(one.score === undefined ? {} : { score: one.score }) });
|
|
145
|
+
return members;
|
|
100
146
|
};
|
|
101
147
|
/**
|
|
102
148
|
* Members are read by what their identities wrote, like `--contact person:<id>` but without the owner's aliases:
|
|
103
149
|
* a short alias would add a text lane that matches mentions, and membership is about who wrote, not who was named.
|
|
104
150
|
*/
|
|
105
|
-
const memberLanesOf = async (store, project, given, room) => {
|
|
151
|
+
const memberLanesOf = async (store, project, given, room, includeProposed) => {
|
|
106
152
|
const lanes = [];
|
|
107
153
|
const left = [];
|
|
108
|
-
|
|
154
|
+
const rows = [];
|
|
155
|
+
for (const member of (await memberRefsOf(store, project, includeProposed)).filter((one) => !given.has(one.ref))) {
|
|
156
|
+
const { ref: reference, ...marks } = member;
|
|
109
157
|
const person = await store.people.personByUid(reference.slice("person:".length));
|
|
110
158
|
const senders = person ? sendersOf(person) : [];
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
else if (lanes.length >= room)
|
|
114
|
-
left.push({ source: reference, kind: "member", reason: "member-limit" });
|
|
115
|
-
else
|
|
159
|
+
const state = !senders.length ? "no-identity" : lanes.length >= room ? "member-limit" : "read";
|
|
160
|
+
if (state === "read")
|
|
116
161
|
lanes.push({ reference, senders });
|
|
162
|
+
else
|
|
163
|
+
left.push({ source: reference, kind: "member", reason: state });
|
|
164
|
+
rows.push({ person: reference, state, ...marks });
|
|
117
165
|
}
|
|
118
|
-
return { lanes, left };
|
|
166
|
+
return { lanes, left, rows };
|
|
119
167
|
};
|
|
120
|
-
|
|
121
|
-
|
|
168
|
+
const chatOf = (from) => {
|
|
169
|
+
if (!from.startsWith("chat:"))
|
|
170
|
+
return undefined;
|
|
171
|
+
const reference = parseReference(from);
|
|
172
|
+
return reference.type === "chat" ? reference : undefined;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Links the owner confirmed, then, when asked, the proposed ones: a suggestion would put a whole chat into the
|
|
176
|
+
* evidence on a guess, so it is read only on request and marked.
|
|
177
|
+
*/
|
|
178
|
+
export const linkedChatsOf = async (store, project, options = {}) => {
|
|
122
179
|
if (!project)
|
|
123
180
|
return [];
|
|
124
181
|
const chats = [];
|
|
125
182
|
for (const relation of await store.knowledge.relations(null, project)) {
|
|
126
183
|
if (!relation.confirmed || relation.kind !== "member-of" || relation.to !== project)
|
|
127
184
|
continue;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const reference = parseReference(relation.from);
|
|
131
|
-
if (reference.type === "chat")
|
|
185
|
+
const reference = chatOf(relation.from);
|
|
186
|
+
if (reference)
|
|
132
187
|
chats.push({
|
|
133
188
|
provider: reference.provider,
|
|
134
189
|
account: reference.account,
|
|
135
190
|
chat: reference.chat,
|
|
136
191
|
role: relation.role,
|
|
192
|
+
purpose: relation.purpose,
|
|
193
|
+
relationship: relation.id,
|
|
137
194
|
});
|
|
138
195
|
}
|
|
196
|
+
if (!options.includeProposed)
|
|
197
|
+
return chats;
|
|
198
|
+
for (const one of await proposedLinksOf(store, project)) {
|
|
199
|
+
const reference = chatOf(one.from);
|
|
200
|
+
if (!reference)
|
|
201
|
+
continue;
|
|
202
|
+
if (chats.some((chat) => chat.provider === reference.provider && chat.account === reference.account && chat.chat === reference.chat))
|
|
203
|
+
continue;
|
|
204
|
+
chats.push({
|
|
205
|
+
provider: reference.provider,
|
|
206
|
+
account: reference.account,
|
|
207
|
+
chat: reference.chat,
|
|
208
|
+
role: one.role,
|
|
209
|
+
purpose: one.purpose,
|
|
210
|
+
relationship: null,
|
|
211
|
+
proposed: one.id,
|
|
212
|
+
...(one.score === undefined ? {} : { score: one.score }),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
139
215
|
return chats;
|
|
140
216
|
};
|
|
217
|
+
/** Over half the items and at least this many, from one chat's membership alone: worth asking what the chat is for. */
|
|
218
|
+
const HINT_ITEMS = 20;
|
|
141
219
|
/**
|
|
142
220
|
* `projects evidence` over a whole store: the project's names, key and aliases, the given words and contacts, its
|
|
143
221
|
* confirmed person members and its confirmed linked chats, in the sources the host resolved.
|
|
@@ -160,9 +238,12 @@ export const gatherStoredProject = async (store, input) => {
|
|
|
160
238
|
(input.keywords?.length ?? 0) +
|
|
161
239
|
contacts.filter((one) => one.name).length;
|
|
162
240
|
const room = Math.min(CORE_LIMIT - contacts.length, CORE_LIMIT - searchLanes - contacts.filter((one) => one.senders).length);
|
|
163
|
-
const
|
|
241
|
+
const includeProposed = input.includeProposed === true;
|
|
242
|
+
const purposes = purposesOf(input.purposes, input.purposeConfig, project);
|
|
243
|
+
const members = await memberLanesOf(store, project.ref, new Set(contacts.flatMap((one) => one.reference ?? [])), room, includeProposed);
|
|
164
244
|
const people = [...contacts, ...members.lanes];
|
|
165
|
-
const
|
|
245
|
+
const all = await linkedChatsOf(store, project.ref, { includeProposed });
|
|
246
|
+
const linked = all.filter((one) => purposeRead(purposes, one.purpose));
|
|
166
247
|
const linkedChats = linked.slice(0, LINKED_CHATS).map(({ provider, account, chat }) => ({ provider, account, chat }));
|
|
167
248
|
const budgets = {
|
|
168
249
|
...(input.maxReads === undefined ? {} : { maxReads: input.maxReads }),
|
|
@@ -208,5 +289,73 @@ export const gatherStoredProject = async (store, input) => {
|
|
|
208
289
|
reason: "linked-chat-limit",
|
|
209
290
|
})),
|
|
210
291
|
];
|
|
211
|
-
|
|
292
|
+
const refOf = (one) => formatReference({ type: "chat", ...one });
|
|
293
|
+
const byRef = new Map(linked.map((one) => [refOf(one), one]));
|
|
294
|
+
const proposedLanes = new Map();
|
|
295
|
+
for (const row of gathered.linkedChats) {
|
|
296
|
+
const proposed = byRef.get(row.ref)?.proposed;
|
|
297
|
+
if (proposed && row.lane !== null)
|
|
298
|
+
proposedLanes.set(row.lane, proposed);
|
|
299
|
+
}
|
|
300
|
+
for (const lane of gathered.lanes) {
|
|
301
|
+
const proposed = members.rows.find((one) => one.proposed && lane.people.includes(one.person))?.proposed;
|
|
302
|
+
if (proposed && lane.kinds.includes("sender"))
|
|
303
|
+
proposedLanes.set(lane.id, proposed);
|
|
304
|
+
}
|
|
305
|
+
// Marked only when nothing but proposed links found it: an item a word or a confirmed link found is evidence anyway.
|
|
306
|
+
const items = gathered.items.map((item) => {
|
|
307
|
+
const proposed = item.lanes.every((lane) => proposedLanes.has(lane))
|
|
308
|
+
? proposedLanes.get(item.lanes[0] ?? "")
|
|
309
|
+
: undefined;
|
|
310
|
+
return proposed ? { ...item, proposed } : item;
|
|
311
|
+
});
|
|
312
|
+
const linkedRows = gathered.linkedChats.map((row) => {
|
|
313
|
+
const link = byRef.get(row.ref);
|
|
314
|
+
return {
|
|
315
|
+
...row,
|
|
316
|
+
purpose: link?.purpose ?? LINK_PURPOSE,
|
|
317
|
+
...(link?.proposed ? { proposed: link.proposed } : {}),
|
|
318
|
+
...(link?.score === undefined ? {} : { score: link.score }),
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
const count = input.count ?? storedChatCount(store);
|
|
322
|
+
const leftOut = [];
|
|
323
|
+
for (const one of all.filter((chat) => !purposeRead(purposes, chat.purpose))) {
|
|
324
|
+
const account = { provider: one.provider, account: one.account };
|
|
325
|
+
const [counted, [held]] = await Promise.all([
|
|
326
|
+
count(account, one.chat, { since: gathered.window.after, until: gathered.window.before }),
|
|
327
|
+
chatCoverage(store, account, [one.chat]),
|
|
328
|
+
]);
|
|
329
|
+
leftOut.push({
|
|
330
|
+
ref: refOf(one),
|
|
331
|
+
provider: one.provider,
|
|
332
|
+
account: one.account,
|
|
333
|
+
chat: one.chat,
|
|
334
|
+
title: held?.title ?? null,
|
|
335
|
+
purpose: one.purpose ?? LINK_PURPOSE,
|
|
336
|
+
relationship: one.relationship,
|
|
337
|
+
...(one.proposed ? { proposed: one.proposed } : {}),
|
|
338
|
+
...counted,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
projectRef: project.ref,
|
|
343
|
+
unreadSources: [...unread, ...unreadChats, ...members.left],
|
|
344
|
+
...gathered,
|
|
345
|
+
items,
|
|
346
|
+
linkedChats: linkedRows,
|
|
347
|
+
mentions,
|
|
348
|
+
purposes: purposes === "all" ? "all" : [...purposes],
|
|
349
|
+
leftOut,
|
|
350
|
+
members: members.rows,
|
|
351
|
+
purposeHint: gathered.resumed ? null : purposeHintOf(linked, linkedRows, items.length),
|
|
352
|
+
};
|
|
353
|
+
};
|
|
354
|
+
const purposeHintOf = (linked, rows, of) => {
|
|
355
|
+
const [top] = [...rows].sort((a, b) => (b.items ?? 0) - (a.items ?? 0));
|
|
356
|
+
const items = top?.items ?? 0;
|
|
357
|
+
const link = linked.find((one) => formatReference({ type: "chat", ...one }) === top?.ref);
|
|
358
|
+
if (!top || !link?.relationship || link.purpose !== null || items < HINT_ITEMS || items * 2 <= of)
|
|
359
|
+
return null;
|
|
360
|
+
return { chat: top.ref, relationship: link.relationship, items, of };
|
|
212
361
|
};
|
package/dist/store/messages.d.ts
CHANGED
|
@@ -45,9 +45,10 @@ export interface AccountMessages {
|
|
|
45
45
|
* ends the window; `reads.MessagePageWindow` says how.
|
|
46
46
|
*/
|
|
47
47
|
messages(key: AccountKey, chatId: Id, window: reads.MessagePageWindow): Promise<Page<Message>>;
|
|
48
|
-
/** How many stored messages the chat has, sent at `since` or later
|
|
48
|
+
/** How many stored messages the chat has, sent at `since` or later and at `until` or earlier when given. */
|
|
49
49
|
countMessages(key: AccountKey, chatId: Id, options?: {
|
|
50
50
|
since?: string;
|
|
51
|
+
until?: string;
|
|
51
52
|
}): Promise<number>;
|
|
52
53
|
/** Saved or edited after `at` (ISO, this machine's clock), oldest first, and the ids deleted after it. */
|
|
53
54
|
changes(key: AccountKey, chatId: Id, at: string): Promise<{
|
package/dist/store/messages.js
CHANGED
|
@@ -32,9 +32,9 @@ export const accountMessagesOver = (context) => {
|
|
|
32
32
|
const chatKey = chatKeyOf(key, chatId);
|
|
33
33
|
return chatKey === undefined ? { items: [], hasMore: false } : reads.messagePage(context, chatKey, window);
|
|
34
34
|
},
|
|
35
|
-
countMessages: async (key, chatId, { since } = {}) => {
|
|
35
|
+
countMessages: async (key, chatId, { since, until } = {}) => {
|
|
36
36
|
const chatKey = chatKeyOf(key, chatId);
|
|
37
|
-
return chatKey === undefined ? 0 : reads.countMessages(context, chatKey, since);
|
|
37
|
+
return chatKey === undefined ? 0 : reads.countMessages(context, chatKey, since, until);
|
|
38
38
|
},
|
|
39
39
|
changes: async (key, chatId, at) => {
|
|
40
40
|
const chatKey = chatKeyOf(key, chatId);
|
|
@@ -59,3 +59,4 @@ export declare const partialMail: (database: CacheDatabase) => PartialMailStore;
|
|
|
59
59
|
export declare const storedWhole: (database: CacheDatabase, emailId: number) => boolean;
|
|
60
60
|
export declare const setLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, found: ReadonlyMap<string, number[]>) => number;
|
|
61
61
|
export declare const forgetLocations: (database: CacheDatabase, accountId: number, folder: string, uidvalidity: string, uids: number[]) => void;
|
|
62
|
+
export declare const backfillSentAt: (database: CacheDatabase, accountId: number, limit: number) => number;
|
|
@@ -107,3 +107,7 @@ export const forgetLocations = (database, accountId, folder, uidvalidity, uids)
|
|
|
107
107
|
for (const emailId of new Set(emails))
|
|
108
108
|
leave.run(emailId, Number(box.id), emailId, Number(box.id));
|
|
109
109
|
};
|
|
110
|
+
export const backfillSentAt = (database, accountId, limit) => Number(database
|
|
111
|
+
.prepare(`UPDATE emails SET sent_at = received_at WHERE id IN (
|
|
112
|
+
SELECT id FROM emails WHERE account_id = ? AND sent_at IS NULL AND received_at IS NOT NULL LIMIT ?)`)
|
|
113
|
+
.run(accountId, limit).changes);
|
|
@@ -238,6 +238,8 @@ export interface MailStore extends PartialMailStore {
|
|
|
238
238
|
read?: boolean;
|
|
239
239
|
flagged?: boolean;
|
|
240
240
|
}>, now: number): Promise<number>;
|
|
241
|
+
/** Gives up to `limit` emails stored without `sent_at` their internal date; answers how many. */
|
|
242
|
+
backfillSentAt(accountId: number, limit: number): Promise<number>;
|
|
241
243
|
/** The live emails whose location in `folder` has this UIDVALIDITY, oldest UID first. */
|
|
242
244
|
folderEmails(accountId: number, folder: string, uidvalidity: string): Promise<FolderEmail[]>;
|
|
243
245
|
/**
|
|
@@ -2,7 +2,7 @@ import { formatLocator } from "../../domain/locator.js";
|
|
|
2
2
|
import { formatReference } from "../../domain/references.js";
|
|
3
3
|
import { fold, normalize } from "../normalize.js";
|
|
4
4
|
import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
|
|
5
|
-
import { folderEmails, forgetLocations, locate, partialMail, setLocations, storedWhole, } from "./email-locations.js";
|
|
5
|
+
import { backfillSentAt, folderEmails, forgetLocations, locate, partialMail, setLocations, storedWhole, } from "./email-locations.js";
|
|
6
6
|
import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
|
|
7
7
|
import { bool, flag, fromJson, int, page, str, toJson } from "./events.js";
|
|
8
8
|
import { folded } from "./folded.js";
|
|
@@ -554,11 +554,8 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
|
|
|
554
554
|
return count;
|
|
555
555
|
});
|
|
556
556
|
},
|
|
557
|
-
async
|
|
558
|
-
|
|
559
|
-
return setEmailFlags(database, accountId, flags, now);
|
|
560
|
-
});
|
|
561
|
-
},
|
|
557
|
+
setFlags: async (accountId, flags, now) => write(database, () => setEmailFlags(database, accountId, flags, now)),
|
|
558
|
+
backfillSentAt: async (accountId, limit) => write(database, () => backfillSentAt(database, accountId, limit)),
|
|
562
559
|
folderEmails: async (accountId, folder, uidvalidity) => folderEmails(database, accountId, folder, uidvalidity),
|
|
563
560
|
setLocations: async (accountId, folder, uidvalidity, found) => write(database, () => setLocations(database, accountId, folder, uidvalidity, found)),
|
|
564
561
|
forgetLocations: async (accountId, folder, uidvalidity, uids) => write(database, () => forgetLocations(database, accountId, folder, uidvalidity, uids)),
|
|
@@ -882,7 +882,7 @@ export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("dri
|
|
|
882
882
|
export declare const newestFirst: SQL<unknown>[];
|
|
883
883
|
export declare const before: (sentAt: number, pk: number) => SQL<unknown>;
|
|
884
884
|
export declare const toMessages: ({ orm }: StoreContext, rows: MessageRow[]) => Message[];
|
|
885
|
-
export declare const countMessages: ({ orm }: StoreContext, chatKey: number, since: string | undefined) => number;
|
|
885
|
+
export declare const countMessages: ({ orm }: StoreContext, chatKey: number, since: string | undefined, until?: string) => number;
|
|
886
886
|
/**
|
|
887
887
|
* What changed in a chat after `at`, a time on this machine's clock: messages first saved or edited
|
|
888
888
|
* since, oldest first, and the ids deleted since. Send time cannot answer it — an edit made today to
|
|
@@ -51,10 +51,10 @@ export const toMessages = ({ orm }, rows) => {
|
|
|
51
51
|
}
|
|
52
52
|
return rows.map((row) => toMessage(row, byMessage.get(row.pk) ?? []));
|
|
53
53
|
};
|
|
54
|
-
export const countMessages = ({ orm }, chatKey, since) => Number(orm
|
|
54
|
+
export const countMessages = ({ orm }, chatKey, since, until) => Number(orm
|
|
55
55
|
.select({ n: sql `count(*)` })
|
|
56
56
|
.from(messages)
|
|
57
|
-
.where(live(chatKey, since === undefined ? undefined : gte(messages.sentAt, toMs(since))))
|
|
57
|
+
.where(live(chatKey, since === undefined ? undefined : gte(messages.sentAt, toMs(since)), until === undefined ? undefined : lte(messages.sentAt, toMs(until))))
|
|
58
58
|
.get()?.n);
|
|
59
59
|
/**
|
|
60
60
|
* What changed in a chat after `at`, a time on this machine's clock: messages first saved or edited
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.554.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
package/parity.json
CHANGED
|
@@ -478,6 +478,9 @@
|
|
|
478
478
|
"meaning": "every chat of these kinds, comma-separated",
|
|
479
479
|
"note": "a folder (`chats folders`) or an export folder (`store export`) takes these chats"
|
|
480
480
|
},
|
|
481
|
+
"--include-proposed": {
|
|
482
|
+
"meaning": "also read chats and members proposed for the project and not yet confirmed, each item marked"
|
|
483
|
+
},
|
|
481
484
|
"--into": {
|
|
482
485
|
"value": "<id>",
|
|
483
486
|
"meaning": "the task that stays: a duplicate is merged into it, or a message is linked to it instead of opening a task"
|
|
@@ -832,6 +835,10 @@
|
|
|
832
835
|
"value": "<url>",
|
|
833
836
|
"meaning": "the tunnel's https address: the browser apps reach mcp serve --http through it, a messenger delivers to bot watch"
|
|
834
837
|
},
|
|
838
|
+
"--purpose": {
|
|
839
|
+
"value": "<purposes>",
|
|
840
|
+
"meaning": "the linked chats' purposes to read, comma-separated, or all; a chat left out is counted"
|
|
841
|
+
},
|
|
835
842
|
"--question": {
|
|
836
843
|
"meaning": "match only questions"
|
|
837
844
|
},
|
|
@@ -3707,9 +3714,11 @@
|
|
|
3707
3714
|
"--contact": "all",
|
|
3708
3715
|
"--context-query": "all",
|
|
3709
3716
|
"--cursor": "all",
|
|
3717
|
+
"--include-proposed": "all",
|
|
3710
3718
|
"--limit": "all",
|
|
3711
3719
|
"--max-pages": "all",
|
|
3712
3720
|
"--no-catch-up": "all",
|
|
3721
|
+
"--purpose": "all",
|
|
3713
3722
|
"--since-time": "all",
|
|
3714
3723
|
"--sync-first": "all",
|
|
3715
3724
|
"--sync-time": "all"
|
|
@@ -3723,6 +3732,7 @@
|
|
|
3723
3732
|
"options": {
|
|
3724
3733
|
"--before-time": "all",
|
|
3725
3734
|
"--context-query": "all",
|
|
3735
|
+
"--purpose": "all",
|
|
3726
3736
|
"--since-time": "all"
|
|
3727
3737
|
}
|
|
3728
3738
|
},
|