@wirecat/cli-core 0.549.0 → 0.551.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 +223 -22
- package/THIRD_PARTY_NOTICES +41 -0
- package/dist/calendar/ics.d.ts +1 -1
- package/dist/calendar/ics.js +5 -2
- package/dist/calendar/windows-zones.generated.d.ts +1 -0
- package/dist/calendar/windows-zones.generated.js +146 -0
- package/dist/cli/compact.js +2 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/meeting-findings-input.js +4 -3
- package/dist/cli/messenger/backfill-command.js +28 -1
- package/dist/cli/messenger/context.js +1 -0
- package/dist/cli/messenger/history-gaps.js +4 -4
- package/dist/cli/messenger/mcp-command.js +1 -0
- package/dist/cli/messenger/messages-delete-command.js +7 -8
- package/dist/cli/messenger/projects-command.js +47 -2
- package/dist/cli/payment-query.d.ts +8 -0
- package/dist/cli/payment-query.js +27 -0
- package/dist/cli/project-status-rows.d.ts +8 -0
- package/dist/cli/project-status-rows.js +102 -0
- package/dist/cli/project-status.d.ts +132 -0
- package/dist/cli/project-status.js +248 -0
- package/dist/cli/steps.js +4 -1
- package/dist/domain/knowledge.d.ts +1 -1
- package/dist/domain/knowledge.js +1 -1
- package/dist/domain/status-words.d.ts +3 -0
- package/dist/domain/status-words.js +44 -0
- package/dist/entities/fact.d.ts +12 -1
- package/dist/entities/fact.js +13 -0
- package/dist/entities/index.d.ts +18 -2
- package/dist/entities/meeting-findings.d.ts +1 -1
- package/dist/entities/message-transfer.d.ts +16 -0
- package/dist/entities/message-transfer.js +6 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mail/partial.d.ts +9 -3
- package/dist/mail/partial.js +35 -10
- package/dist/mail/source.d.ts +7 -1
- package/dist/mail/sync.js +1 -1
- package/dist/mcp/app-server.js +1 -0
- package/dist/mcp/http/oauth.d.ts +2 -1
- package/dist/mcp/http/oauth.js +2 -2
- package/dist/mcp/http/serve.d.ts +2 -0
- package/dist/mcp/http/serve.js +2 -1
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/surface.js +1 -1
- package/dist/meetings/findings-rules.d.ts +14 -3
- package/dist/meetings/findings-rules.js +16 -2
- package/dist/meetings/index.d.ts +3 -2
- package/dist/meetings/index.js +2 -1
- package/dist/meetings/meeting-context.d.ts +2 -3
- package/dist/meetings/meeting-context.js +3 -4
- package/dist/meetings/payment.d.ts +27 -0
- package/dist/meetings/payment.js +81 -0
- package/dist/services/archive.d.ts +13 -0
- package/dist/services/archive.js +56 -5
- package/dist/services/deps.d.ts +3 -0
- package/dist/services/history-limits.d.ts +2 -0
- package/dist/services/index.d.ts +4 -1
- package/dist/services/index.js +4 -1
- package/dist/services/job-query.d.ts +5 -2
- package/dist/services/job-query.js +5 -2
- package/dist/services/link-scorers/alias-hits.d.ts +7 -0
- package/dist/services/link-scorers/alias-hits.js +54 -0
- package/dist/services/link-scorers/chat-senders.d.ts +6 -0
- package/dist/services/link-scorers/chat-senders.js +56 -0
- package/dist/services/link-scorers/chat-title.d.ts +6 -0
- package/dist/services/link-scorers/chat-title.js +44 -0
- package/dist/services/link-scorers/email-domain.d.ts +6 -0
- package/dist/services/link-scorers/email-domain.js +64 -0
- package/dist/services/link-scorers/index.d.ts +9 -0
- package/dist/services/link-scorers/index.js +8 -0
- package/dist/services/link-scorers/mail-thread.d.ts +3 -0
- package/dist/services/link-scorers/mail-thread.js +74 -0
- package/dist/services/link-scorers/member-chats.d.ts +3 -0
- package/dist/services/link-scorers/member-chats.js +40 -0
- package/dist/services/link-scorers/member-dialog.d.ts +3 -0
- package/dist/services/link-scorers/member-dialog.js +34 -0
- package/dist/services/link-scorers/scan.d.ts +41 -0
- package/dist/services/link-scorers/scan.js +244 -0
- package/dist/services/link-scorers/shared.d.ts +26 -0
- package/dist/services/link-scorers/shared.js +48 -0
- package/dist/services/link-scorers/types.d.ts +77 -0
- package/dist/services/link-scorers/types.js +1 -0
- package/dist/services/project-status/index.d.ts +71 -0
- package/dist/services/project-status/index.js +196 -0
- package/dist/services/project-status/items.d.ts +10 -0
- package/dist/services/project-status/items.js +23 -0
- package/dist/services/project-status/signals/amounts.d.ts +20 -0
- package/dist/services/project-status/signals/amounts.js +91 -0
- package/dist/services/project-status/signals/children.d.ts +13 -0
- package/dist/services/project-status/signals/children.js +28 -0
- package/dist/services/project-status/signals/coverage.d.ts +13 -0
- package/dist/services/project-status/signals/coverage.js +30 -0
- package/dist/services/project-status/signals/index.d.ts +3 -0
- package/dist/services/project-status/signals/index.js +21 -0
- package/dist/services/project-status/signals/last-message.d.ts +4 -0
- package/dist/services/project-status/signals/last-message.js +22 -0
- package/dist/services/project-status/signals/payments.d.ts +9 -0
- package/dist/services/project-status/signals/payments.js +24 -0
- package/dist/services/project-status/signals/promises.d.ts +17 -0
- package/dist/services/project-status/signals/promises.js +109 -0
- package/dist/services/project-status/signals/questions.d.ts +8 -0
- package/dist/services/project-status/signals/questions.js +38 -0
- package/dist/services/project-status/signals/reply-next.d.ts +19 -0
- package/dist/services/project-status/signals/reply-next.js +75 -0
- package/dist/services/project-status/signals/topics.d.ts +10 -0
- package/dist/services/project-status/signals/topics.js +50 -0
- package/dist/services/project-status/types.d.ts +84 -0
- package/dist/services/project-status/types.js +1 -0
- package/dist/services/proposals/index.d.ts +1 -0
- package/dist/services/proposals/index.js +1 -0
- package/dist/services/proposals/kinds/findings.d.ts +3 -0
- package/dist/services/proposals/kinds/findings.js +8 -1
- package/dist/store/decisions.d.ts +5 -1
- package/dist/store/decisions.js +5 -1
- package/dist/store/evidence-findings.js +1 -0
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.js +1 -0
- package/dist/store/project-transfer/request.d.ts +1 -1
- package/dist/store/project-transfer/schema.d.ts +2 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
- package/dist/store/sqlite/email-locations.d.ts +22 -1
- package/dist/store/sqlite/email-locations.js +29 -2
- package/dist/store/sqlite/emails.d.ts +3 -7
- package/dist/store/sqlite/emails.js +4 -2
- package/dist/store/sqlite/evidence-findings.js +5 -1
- package/dist/store/sqlite/facts.d.ts +13 -0
- package/dist/store/sqlite/facts.js +64 -6
- package/dist/store/sqlite/findings-core.js +14 -1
- package/dist/store/sqlite/knowledge.d.ts +16 -2
- package/dist/store/sqlite/knowledge.js +1 -0
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
- package/dist/store/sqlite/relation-proposals.d.ts +29 -2
- package/dist/store/sqlite/relation-proposals.js +75 -4
- package/dist/store/store.d.ts +1 -0
- package/package.json +3 -3
- package/parity.json +11 -0
package/dist/mail/partial.js
CHANGED
|
@@ -6,15 +6,29 @@ import { parseMail } from "./parse.js";
|
|
|
6
6
|
export const PARTIAL = "partial";
|
|
7
7
|
/** How many partial emails one sync reads whole at most; the rest wait for the next run. */
|
|
8
8
|
export const PARTIAL_PER_RUN = 100;
|
|
9
|
+
/** Failed reads after which a partial email is given up on (`partial.stuck`) and stays as it is. */
|
|
10
|
+
export const PARTIAL_TRIES = 3;
|
|
9
11
|
/**
|
|
10
12
|
* Reads whole the emails a server search stored from their first bytes, so the archive holds their attachments too.
|
|
11
|
-
* Sync never reads stored mail again, so without this step a partial email would stay partial.
|
|
12
|
-
*
|
|
13
|
+
* Sync never reads stored mail again, so without this step a partial email would stay partial. A source with `find`
|
|
14
|
+
* finds them by id; elsewhere they are read at their newest stored location under its UIDVALIDITY, trusted only when
|
|
15
|
+
* `identify` gives the stored id for the bytes read there. Each failed read is counted, and after `PARTIAL_TRIES`, or
|
|
16
|
+
* at once when the place holds another message, the email is left partial for good.
|
|
13
17
|
*/
|
|
14
18
|
export const completePartial = async ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, loadEngine, now, take, signal, }) => {
|
|
15
|
-
const { emails, total } = await store.mail.partialEmails(accountId, PARTIAL_PER_RUN);
|
|
19
|
+
const { emails, total, stuck } = await store.mail.partialEmails(accountId, PARTIAL_PER_RUN);
|
|
16
20
|
if (emails.length === 0)
|
|
17
|
-
return { completed: 0, left: 0 };
|
|
21
|
+
return { completed: 0, left: 0, stuck };
|
|
22
|
+
let given = 0;
|
|
23
|
+
// A failed read is counted, so an email the server no longer gives is not read whole on every sync forever.
|
|
24
|
+
const failed = async (email, forGood = false) => {
|
|
25
|
+
const mark = (email.metadata?.[PARTIAL] ?? {});
|
|
26
|
+
const tries = Number(mark.tries ?? 0) + 1;
|
|
27
|
+
const gaveUp = forGood || tries >= PARTIAL_TRIES;
|
|
28
|
+
await store.mail.markPartial(accountId, email.externalId, { ...mark, tries, ...(gaveUp ? { stuck: true } : {}) });
|
|
29
|
+
if (gaveUp)
|
|
30
|
+
given++;
|
|
31
|
+
};
|
|
18
32
|
const byId = new Map(emails.map((email) => [email.externalId, email]));
|
|
19
33
|
const items = new Map();
|
|
20
34
|
if (source.find) {
|
|
@@ -23,21 +37,27 @@ export const completePartial = async ({ store, source, accountId, address, folde
|
|
|
23
37
|
if (left.length === 0)
|
|
24
38
|
break;
|
|
25
39
|
if (take?.())
|
|
26
|
-
return { completed: 0, left: total };
|
|
40
|
+
return { completed: 0, left: total, stuck };
|
|
27
41
|
for (const found of await source.find(left, folder)) {
|
|
28
42
|
const email = byId.get(found.messageId);
|
|
29
43
|
if (email && !items.has(email))
|
|
30
44
|
items.set(email, found);
|
|
31
45
|
}
|
|
32
46
|
}
|
|
47
|
+
for (const email of emails)
|
|
48
|
+
if (!items.has(email))
|
|
49
|
+
await failed(email);
|
|
33
50
|
}
|
|
34
51
|
else if (source.identify)
|
|
35
52
|
for (const email of emails) {
|
|
36
53
|
const place = email.locations[0];
|
|
37
|
-
if (place)
|
|
54
|
+
if (!place)
|
|
55
|
+
await failed(email, true);
|
|
56
|
+
else
|
|
38
57
|
items.set(email, {
|
|
39
58
|
uid: String(place.uid),
|
|
40
59
|
folder: place.folder,
|
|
60
|
+
validity: place.uidvalidity,
|
|
41
61
|
receivedAt: new Date(email.receivedAt ?? 0).toISOString(),
|
|
42
62
|
});
|
|
43
63
|
}
|
|
@@ -58,11 +78,15 @@ export const completePartial = async ({ store, source, accountId, address, folde
|
|
|
58
78
|
const attachments = [];
|
|
59
79
|
for (const [index, [email, item]] of chunk.entries()) {
|
|
60
80
|
const raw = raws[index];
|
|
61
|
-
if (!(raw instanceof Uint8Array))
|
|
81
|
+
if (!(raw instanceof Uint8Array)) {
|
|
82
|
+
await failed(email);
|
|
62
83
|
continue;
|
|
63
|
-
|
|
64
|
-
|
|
84
|
+
}
|
|
85
|
+
// Only the stored id proves the bytes at a stored UID are this email; another one there will not change back.
|
|
86
|
+
if (!source.find && source.identify?.(raw, item).messageId !== email.externalId) {
|
|
87
|
+
await failed(email, true);
|
|
65
88
|
continue;
|
|
89
|
+
}
|
|
66
90
|
try {
|
|
67
91
|
const mail = await parseMail(raw, maxBodyChars);
|
|
68
92
|
const files = await readAttachments(mail, email.externalId, { loadEngine, maxAttachmentChars, at: now, ...(signal ? { signal } : {}) }, attachments);
|
|
@@ -87,8 +111,9 @@ export const completePartial = async ({ store, source, accountId, address, folde
|
|
|
87
111
|
catch {
|
|
88
112
|
if (signal?.aborted)
|
|
89
113
|
break;
|
|
114
|
+
await failed(email);
|
|
90
115
|
}
|
|
91
116
|
}
|
|
92
117
|
}
|
|
93
|
-
return { completed, left: total - completed };
|
|
118
|
+
return { completed, left: total - completed - given, stuck: stuck + given };
|
|
94
119
|
};
|
package/dist/mail/source.d.ts
CHANGED
|
@@ -24,7 +24,13 @@ export interface Listed {
|
|
|
24
24
|
headers?: MailHeaders;
|
|
25
25
|
}
|
|
26
26
|
/** A message as `quick` lists it: everything but the ids, which a source without a cheap one reads from the raw bytes. */
|
|
27
|
-
export type Quick = Omit<Listed, "messageId" | "threadId"
|
|
27
|
+
export type Quick = Omit<Listed, "messageId" | "threadId"> & {
|
|
28
|
+
/**
|
|
29
|
+
* The folder's UIDVALIDITY the UID was stored under. `fetchRaw` reads nothing for it when the folder's differs, and
|
|
30
|
+
* `identify` works the ids out under it.
|
|
31
|
+
*/
|
|
32
|
+
validity?: string;
|
|
33
|
+
};
|
|
28
34
|
export interface Searched {
|
|
29
35
|
/** Absent when the server does not say; then no UID is trusted across runs. */
|
|
30
36
|
validity?: string;
|
package/dist/mail/sync.js
CHANGED
|
@@ -54,7 +54,7 @@ export const syncMail = async (source, store, options) => {
|
|
|
54
54
|
...(options.budget ? { take: () => options.budget?.take() ?? null } : {}),
|
|
55
55
|
...(options.signal ? { signal: options.signal } : {}),
|
|
56
56
|
});
|
|
57
|
-
return partial.completed || partial.left ? { ...result, partial } : result;
|
|
57
|
+
return partial.completed || partial.left || partial.stuck ? { ...result, partial } : result;
|
|
58
58
|
};
|
|
59
59
|
const syncRounds = async (source, store, options) => {
|
|
60
60
|
const { budget, index } = options;
|
package/dist/mcp/app-server.js
CHANGED
|
@@ -144,6 +144,7 @@ export const serveAppOverHttp = async (command, context, options, http) => {
|
|
|
144
144
|
const listening = await serveOverHttp(build, {
|
|
145
145
|
...http,
|
|
146
146
|
appName: app.command,
|
|
147
|
+
reaches: `your ${options.name}`,
|
|
147
148
|
onCode: (code, expires) => context.renderer.note(context.env[mcpHttpMarker(app.envPrefix)] === undefined
|
|
148
149
|
? `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`
|
|
149
150
|
: `a new login code for a browser app: \`${app.command} ${context.settings.profile} mcp server status\``),
|
package/dist/mcp/http/oauth.d.ts
CHANGED
|
@@ -14,11 +14,12 @@ export interface OwnerLogin {
|
|
|
14
14
|
* the browser apps need dynamic registration — but nobody gets a token without the one-time code
|
|
15
15
|
* printed in the owner's terminal. Five wrong codes delay further attempts for thirty seconds.
|
|
16
16
|
*/
|
|
17
|
-
export declare const ownerLogin: ({ issuer, resource, file, appName, onCode, now, }: {
|
|
17
|
+
export declare const ownerLogin: ({ issuer, resource, file, appName, reaches, onCode, now, }: {
|
|
18
18
|
issuer: URL;
|
|
19
19
|
resource: URL;
|
|
20
20
|
file: string;
|
|
21
21
|
appName: string;
|
|
22
|
+
reaches?: string;
|
|
22
23
|
/** The code for the next login, for the owner's eyes only: their terminal, or the server's status file. */
|
|
23
24
|
onCode: (code: string, expires: Date) => void;
|
|
24
25
|
now?: () => number;
|
package/dist/mcp/http/oauth.js
CHANGED
|
@@ -51,7 +51,7 @@ const redirectAllowed = (uri) => {
|
|
|
51
51
|
* the browser apps need dynamic registration — but nobody gets a token without the one-time code
|
|
52
52
|
* printed in the owner's terminal. Five wrong codes delay further attempts for thirty seconds.
|
|
53
53
|
*/
|
|
54
|
-
export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () => Date.now(), }) => {
|
|
54
|
+
export const ownerLogin = ({ issuer, resource, file, appName, reaches = `your ${appName} data`, onCode, now = () => Date.now(), }) => {
|
|
55
55
|
const store = tokenFile(file);
|
|
56
56
|
const codes = new Map();
|
|
57
57
|
let login = { value: "", expires: 0 };
|
|
@@ -153,7 +153,7 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
153
153
|
.join("");
|
|
154
154
|
const who = escapeHtml(client.name ?? "an application");
|
|
155
155
|
const target = new URL(params.get("redirect_uri") ?? "");
|
|
156
|
-
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(appName)} login</title><style>body{font:16px system-ui,sans-serif;max-width:28rem;margin:3rem auto;padding:0 1rem;color:#1d2430}input[type=text]{font:20px ui-monospace,monospace;letter-spacing:.1em;width:100%;padding:.5rem;margin:.5rem 0 1rem;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem}.error{color:#b42318}</style></head><body><h1>${escapeHtml(appName)}</h1><p>An app that calls itself <strong>${who}</strong> asks to reach
|
|
156
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(appName)} login</title><style>body{font:16px system-ui,sans-serif;max-width:28rem;margin:3rem auto;padding:0 1rem;color:#1d2430}input[type=text]{font:20px ui-monospace,monospace;letter-spacing:.1em;width:100%;padding:.5rem;margin:.5rem 0 1rem;box-sizing:border-box}button{font:inherit;padding:.5rem 1rem}.error{color:#b42318}</style></head><body><h1>${escapeHtml(appName)}</h1><p>An app that calls itself <strong>${who}</strong> asks to reach ${escapeHtml(reaches)} through <code>${escapeHtml(appName)} mcp serve --http</code>.</p><p>After you allow it, the login goes to <strong>${escapeHtml(target.host)}</strong>. Allow only if that is the app you are connecting, such as chatgpt.com or claude.ai.</p><p>Type the code shown in WireCat Desktop, or in the terminal where the server runs. If you did not start this login, close this page.</p>${message ? `<p class="error">${escapeHtml(message)}</p>` : ""}<form method="post" action="${escapeHtml(`${base}/authorize`)}">${hidden}<label>Login code<input type="text" name="login_code" autocomplete="off" autofocus required></label><button type="submit">Allow</button></form></body></html>`;
|
|
157
157
|
return new Response(html, {
|
|
158
158
|
status: message ? 400 : 200,
|
|
159
159
|
headers: {
|
package/dist/mcp/http/serve.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface HttpOptions {
|
|
|
13
13
|
/** Where the hashes of issued tokens and registered clients are kept. */
|
|
14
14
|
tokenFile: string;
|
|
15
15
|
appName: string;
|
|
16
|
+
/** What the browser login page says an app asks to reach: `your messages`, `your Google Calendar`. */
|
|
17
|
+
reaches?: string;
|
|
16
18
|
onCode: (code: string, expires: Date) => void;
|
|
17
19
|
onError?: (error: Error) => void;
|
|
18
20
|
now?: () => number;
|
package/dist/mcp/http/serve.js
CHANGED
|
@@ -91,7 +91,7 @@ const toolsIn = async (request) => {
|
|
|
91
91
|
* the owner's browser cannot reach the port through a rebound DNS name.
|
|
92
92
|
*/
|
|
93
93
|
export const serveOverHttp = async (build, options) => {
|
|
94
|
-
const { publicUrl, port, tokenFile, appName, onCode, onError, now, lockFile, version } = options;
|
|
94
|
+
const { publicUrl, port, tokenFile, appName, reaches, onCode, onError, now, lockFile, version } = options;
|
|
95
95
|
const lock = lockFile
|
|
96
96
|
? httpLock(lockFile, {
|
|
97
97
|
pid: process.pid,
|
|
@@ -114,6 +114,7 @@ export const serveOverHttp = async (build, options) => {
|
|
|
114
114
|
resource,
|
|
115
115
|
file: tokenFile,
|
|
116
116
|
appName,
|
|
117
|
+
...(reaches ? { reaches } : {}),
|
|
117
118
|
onCode: (code, expires) => {
|
|
118
119
|
lock?.update({ loginCode: code, loginCodeExpiresAt: expires.toISOString() });
|
|
119
120
|
onCode(code, expires);
|
package/dist/mcp/server.js
CHANGED
|
@@ -417,6 +417,7 @@ export const serveOverHttpUntilStopped = async (command, context, messenger, opt
|
|
|
417
417
|
const listening = await serveOverHttp(build, {
|
|
418
418
|
...http,
|
|
419
419
|
appName,
|
|
420
|
+
reaches: "your messages",
|
|
420
421
|
// In the background its log is a file; the code goes only to the lock that `mcp server status` reads.
|
|
421
422
|
onCode: (code, expires) => context.renderer.note(context.env[mcpHttpMarker(appEnvPrefix)] === undefined
|
|
422
423
|
? `login code for a new browser app: ${code} (until ${expires.toTimeString().slice(0, 5)}; a new one after each login)`
|
package/dist/mcp/surface.js
CHANGED
|
@@ -46,7 +46,7 @@ const runnableOnly = (result, offered) => {
|
|
|
46
46
|
const body = result.structuredContent;
|
|
47
47
|
if (result.isError || !Array.isArray(body?.next))
|
|
48
48
|
return result;
|
|
49
|
-
const next = body.next.filter((step) => offered(step.command));
|
|
49
|
+
const next = body.next.filter((step) => step.command === undefined || offered(step.command));
|
|
50
50
|
const { next: _, ...rest } = body;
|
|
51
51
|
const kept = next.length > 0 ? { ...rest, next } : rest;
|
|
52
52
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FACT_TYPES, type Payment } from "./payment.js";
|
|
2
2
|
declare const TASK_TYPES: readonly ["bug", "feature", "chore"];
|
|
3
3
|
export interface FindingEvidence {
|
|
4
4
|
reference: string;
|
|
@@ -14,9 +14,18 @@ interface FindingBase {
|
|
|
14
14
|
}
|
|
15
15
|
export interface FactFinding extends FindingBase {
|
|
16
16
|
kind: "fact";
|
|
17
|
-
type: (typeof FACT_TYPES)[number]
|
|
17
|
+
type: Exclude<(typeof FACT_TYPES)[number], "payment">;
|
|
18
18
|
statement: string;
|
|
19
19
|
}
|
|
20
|
+
/** A payment an agent read; confirming it records a `payment` fact. A paid one supersedes the expected one. */
|
|
21
|
+
export interface PaymentFinding extends FindingBase {
|
|
22
|
+
kind: "payment";
|
|
23
|
+
/** The fact's full sentence; without one, the payment's title. */
|
|
24
|
+
statement?: string;
|
|
25
|
+
payment: Payment;
|
|
26
|
+
/** The `fact:` of the expected payment this one replaces, once it is paid or cancelled. */
|
|
27
|
+
supersedes?: string;
|
|
28
|
+
}
|
|
20
29
|
export interface TaskFinding extends FindingBase {
|
|
21
30
|
kind: "task";
|
|
22
31
|
type: (typeof TASK_TYPES)[number];
|
|
@@ -28,7 +37,7 @@ export interface TaskFinding extends FindingBase {
|
|
|
28
37
|
assignee?: string;
|
|
29
38
|
assigneeEvidence?: string[];
|
|
30
39
|
}
|
|
31
|
-
export type Finding = FactFinding | TaskFinding;
|
|
40
|
+
export type Finding = FactFinding | TaskFinding | PaymentFinding;
|
|
32
41
|
export interface FindingsInput {
|
|
33
42
|
schemaVersion: 1;
|
|
34
43
|
key: string;
|
|
@@ -59,6 +68,8 @@ export interface FindingsRules {
|
|
|
59
68
|
reference: (raw: string) => string;
|
|
60
69
|
/** A fact finding may name no project, and any finding may name a subject. */
|
|
61
70
|
anySubject?: boolean;
|
|
71
|
+
/** Payment findings are staged on a project's evidence, not from a meeting. */
|
|
72
|
+
payments?: boolean;
|
|
62
73
|
}
|
|
63
74
|
export declare const validateFindings: (input: FindingsInput, rules: FindingsRules) => FindingsInput;
|
|
64
75
|
export declare const findingFingerprint: ({ id: _id, ...finding }: Finding) => string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
|
-
import {
|
|
3
|
+
import { FACT_TYPES, validatePayment } from "./payment.js";
|
|
4
4
|
import { meetingTimestamp } from "./timestamp.js";
|
|
5
5
|
const TASK_TYPES = ["bug", "feature", "chore"];
|
|
6
6
|
const invalid = (message) => {
|
|
@@ -69,12 +69,26 @@ export const validateFindings = (input, rules) => {
|
|
|
69
69
|
};
|
|
70
70
|
if (finding.kind === "fact") {
|
|
71
71
|
fields(finding, ["id", "project", "subject", "evidence", "kind", "type", "statement"], rules.singular);
|
|
72
|
+
if (finding.type === "payment")
|
|
73
|
+
return invalid("a payment is a finding of kind payment");
|
|
72
74
|
if (!FACT_TYPES.includes(finding.type))
|
|
73
75
|
return invalid("unknown fact finding type");
|
|
74
76
|
return { ...base, kind: "fact", type: finding.type, statement: text(finding.statement, 2000) };
|
|
75
77
|
}
|
|
78
|
+
if (finding.kind === "payment" && rules.payments) {
|
|
79
|
+
fields(finding, ["id", "project", "subject", "evidence", "kind", "statement", "payment", "supersedes"], rules.singular);
|
|
80
|
+
if (finding.supersedes !== undefined && !/^fact:[1-9]\d*$/.test(String(finding.supersedes)))
|
|
81
|
+
return invalid("a payment finding supersedes a fact: reference");
|
|
82
|
+
return {
|
|
83
|
+
...base,
|
|
84
|
+
kind: "payment",
|
|
85
|
+
...(finding.statement === undefined ? {} : { statement: text(finding.statement, 2000) }),
|
|
86
|
+
payment: validatePayment(finding.payment),
|
|
87
|
+
...(finding.supersedes === undefined ? {} : { supersedes: finding.supersedes }),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
76
90
|
if (finding.kind !== "task")
|
|
77
|
-
return invalid(
|
|
91
|
+
return invalid(`a finding is a fact${rules.payments ? ", payment" : ""} or task`);
|
|
78
92
|
fields(finding, [
|
|
79
93
|
"id",
|
|
80
94
|
"project",
|
package/dist/meetings/index.d.ts
CHANGED
|
@@ -9,17 +9,18 @@ export { entryInWindow, eventChange } from "./event-store.js";
|
|
|
9
9
|
export { linkEvent } from "./events.js";
|
|
10
10
|
export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
|
|
11
11
|
export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
|
|
12
|
-
export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFingerprint, validateFindings, } from "./findings-rules.js";
|
|
12
|
+
export { type Finding, type FindingsInput, type FindingsReceipt, type FindingsRules, findingFingerprint, type PaymentFinding, validateFindings, } from "./findings-rules.js";
|
|
13
13
|
export { liftMeetingFields, MEETING_HIT_QUERY, meetingWordsQuery, parseMeetingQuery } from "./hit-query.js";
|
|
14
14
|
export { type ImportFile, importFiles } from "./import.js";
|
|
15
15
|
export { MEETING_ISSUES, MEETING_QUERY, type MeetingIssue, type MeetingItem, type MeetingOwner, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
|
|
16
16
|
export { joinKeyOf, joinUrlWithoutQuery } from "./join-key.js";
|
|
17
17
|
export { CHAT_MESSAGES_READ, type MeetingChatLine, type MeetingChatLinesInput, type MeetingChatLinesStore, type MeetingLink, } from "./links.js";
|
|
18
|
-
export { createMeetingContext,
|
|
18
|
+
export { createMeetingContext, type MeetingContextAction, type MeetingContextChange, type MeetingContextOperation, type MeetingContextPorts, type MeetingContextReceipt, validateMeetingContextChange, } from "./meeting-context.js";
|
|
19
19
|
export { createMeetingManagement, dispatchMeetingMutation, type ManagedMeeting, type ManagedMeetingChange, type MeetingManagement, type MeetingManagementPorts, type MeetingMutationOptions, type MeetingMutationProposal, type MeetingMutationReceipt, type MeetingOperationOptions, type MeetingProviderContext, validateMeetingChange, } from "./meeting-management.js";
|
|
20
20
|
export type { Attachment, ChatLine, Event, EventSeries, JsonValue, Meeting, MeetingSeries, Metadata, Occurrence, Participant, RecordTimes, SourceChatLine, SourceMeeting, Summary, SummaryContent, Transcript, TranscriptLine, TranscriptRow, } from "./model.js";
|
|
21
21
|
export { type MeetingParticipantCandidate, type MeetingParticipantPage, type MeetingParticipantReader, type MeetingParticipantSearchOptions, type MeetingParticipantSearchStore, normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
|
|
22
22
|
export { type MeetingContextCue, type MeetingHitContextOptions, type MeetingHitContextPacket, type MeetingPassageOutputFormat, type MeetingPassageReader, type MeetingPassageRequest, type MeetingPassageResult, meetingHitContextLimits, validateMeetingPassage, } from "./passage.js";
|
|
23
|
+
export { FACT_TYPES, PAYMENT_DIRECTIONS, PAYMENT_STATES, type Payment, paymentTitle, validatePayment, } from "./payment.js";
|
|
23
24
|
export { type MeetingPreparationContext, type MeetingPreparationContextRequest, type MeetingPreparationContextStore, type PreparationPage, validateMeetingPreparationContext, } from "./preparation-context.js";
|
|
24
25
|
export type { MeetingPreparationFact, MeetingPreparationKnowledgeRequest, MeetingPreparationKnowledgeStore, } from "./preparation-knowledge.js";
|
|
25
26
|
export type { MeetingPreparationNote, MeetingPreparationNotesSelection, MeetingPreparationNotesStore, } from "./preparation-notes.js";
|
package/dist/meetings/index.js
CHANGED
|
@@ -12,10 +12,11 @@ export { importFiles } from "./import.js";
|
|
|
12
12
|
export { MEETING_ISSUES, MEETING_QUERY, meeting, meetingIssuesOf, meetingItem, } from "./item.js";
|
|
13
13
|
export { joinKeyOf, joinUrlWithoutQuery } from "./join-key.js";
|
|
14
14
|
export { CHAT_MESSAGES_READ, } from "./links.js";
|
|
15
|
-
export { createMeetingContext,
|
|
15
|
+
export { createMeetingContext, validateMeetingContextChange, } from "./meeting-context.js";
|
|
16
16
|
export { createMeetingManagement, dispatchMeetingMutation, validateMeetingChange, } from "./meeting-management.js";
|
|
17
17
|
export { normalizeMeetingParticipantQuery, validateMeetingParticipantSearch, } from "./participant-search.js";
|
|
18
18
|
export { meetingHitContextLimits, validateMeetingPassage, } from "./passage.js";
|
|
19
|
+
export { FACT_TYPES, PAYMENT_DIRECTIONS, PAYMENT_STATES, paymentTitle, validatePayment, } from "./payment.js";
|
|
19
20
|
export { validateMeetingPreparationContext, } from "./preparation-context.js";
|
|
20
21
|
export { readMeetingProjectEvidence } from "./project-evidence.js";
|
|
21
22
|
export { meetingProjectEvidence, meetingProjectEvidenceAnswer, } from "./project-evidence-answer.js";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type MeetingMutationOptions } from "./meeting-management.js";
|
|
2
|
+
import { FACT_TYPES } from "./payment.js";
|
|
2
3
|
import { type MeetingProviderScope } from "./provider.js";
|
|
3
|
-
/** Mirrors the store's fact types; the store checks the type again when it writes. */
|
|
4
|
-
export declare const MEETING_FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise"];
|
|
5
4
|
export type MeetingContextChange = {
|
|
6
5
|
action: "note.add";
|
|
7
6
|
text: string;
|
|
@@ -19,7 +18,7 @@ export type MeetingContextChange = {
|
|
|
19
18
|
} | {
|
|
20
19
|
action: "fact.add";
|
|
21
20
|
statement: string;
|
|
22
|
-
type: (typeof
|
|
21
|
+
type: (typeof FACT_TYPES)[number];
|
|
23
22
|
scope: "personal" | "work";
|
|
24
23
|
evidence: readonly string[];
|
|
25
24
|
subject?: string;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { dispatchMeetingMutation } from "./meeting-management.js";
|
|
3
|
+
import { FACT_TYPES } from "./payment.js";
|
|
3
4
|
import { snapshotMeetingScope } from "./provider.js";
|
|
4
|
-
/** Mirrors the store's fact types; the store checks the type again when it writes. */
|
|
5
|
-
export const MEETING_FACT_TYPES = ["fact", "statement", "question", "decision", "promise"];
|
|
6
5
|
const id = (value) => {
|
|
7
6
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
|
|
8
7
|
throw new CliError("validation_error", "meeting context identifiers must be positive integers");
|
|
@@ -63,8 +62,8 @@ export const validateMeetingContextChange = (change) => {
|
|
|
63
62
|
});
|
|
64
63
|
}
|
|
65
64
|
case "fact.add": {
|
|
66
|
-
if (!
|
|
67
|
-
throw new CliError("validation_error", `fact type (${
|
|
65
|
+
if (!FACT_TYPES.includes(change.type) || !["personal", "work"].includes(change.scope))
|
|
66
|
+
throw new CliError("validation_error", `fact type (${FACT_TYPES.join(", ")}) and explicit personal/work scope are required`);
|
|
68
67
|
return Object.freeze({
|
|
69
68
|
action: change.action,
|
|
70
69
|
statement: text(change.statement, 20_000),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Every fact type the store takes; findings and meeting changes check against the same list. */
|
|
2
|
+
export declare const FACT_TYPES: readonly ["fact", "statement", "question", "decision", "promise", "payment"];
|
|
3
|
+
export declare const PAYMENT_DIRECTIONS: readonly ["to_us", "from_us"];
|
|
4
|
+
export declare const PAYMENT_STATES: readonly ["expected", "paid", "cancelled"];
|
|
5
|
+
/** A payment fact's fields, kept in `facts.metadata.payment`. */
|
|
6
|
+
export interface Payment {
|
|
7
|
+
/** A decimal as text with two places, "50000.00": money is never a float. */
|
|
8
|
+
amount: string;
|
|
9
|
+
/** ISO 4217, "RUB". */
|
|
10
|
+
currency: string;
|
|
11
|
+
direction: (typeof PAYMENT_DIRECTIONS)[number];
|
|
12
|
+
/** `person:`, `organization:` or `project:`; `null` when only a name is known. */
|
|
13
|
+
counterparty: string | null;
|
|
14
|
+
counterparty_name: string | null;
|
|
15
|
+
/** An ISO date. */
|
|
16
|
+
due_at: string | null;
|
|
17
|
+
/** A change of state is a new fact that supersedes this one, so the history and its evidence stay. */
|
|
18
|
+
state: (typeof PAYMENT_STATES)[number];
|
|
19
|
+
/** What it is for, short: "support, September". The fact's statement is the full sentence. */
|
|
20
|
+
item: string | null;
|
|
21
|
+
}
|
|
22
|
+
/** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
|
|
23
|
+
export declare const validatePayment: (input: unknown) => Payment;
|
|
24
|
+
/** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
|
|
25
|
+
export declare const paymentTitle: (fact: {
|
|
26
|
+
payment: Payment | null;
|
|
27
|
+
}) => string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
/** Every fact type the store takes; findings and meeting changes check against the same list. */
|
|
3
|
+
export const FACT_TYPES = ["fact", "statement", "question", "decision", "promise", "payment"];
|
|
4
|
+
export const PAYMENT_DIRECTIONS = ["to_us", "from_us"];
|
|
5
|
+
export const PAYMENT_STATES = ["expected", "paid", "cancelled"];
|
|
6
|
+
const invalid = (message) => {
|
|
7
|
+
throw new CliError("validation_error", message);
|
|
8
|
+
};
|
|
9
|
+
const CURRENCIES = new Set(Intl.supportedValuesOf("currency"));
|
|
10
|
+
const AMOUNT = /^(\d{1,15})(?:\.(\d{1,2}))?$/;
|
|
11
|
+
const COUNTERPARTY = /^(person|organization|project):[1-9]\d*$/;
|
|
12
|
+
const amountOf = (value) => {
|
|
13
|
+
const match = AMOUNT.exec(typeof value === "number" ? String(value) : typeof value === "string" ? value.trim() : "");
|
|
14
|
+
if (!match)
|
|
15
|
+
return invalid("a payment's amount is a positive number with at most 2 decimals, such as 1200.50");
|
|
16
|
+
const amount = `${BigInt(match[1] ?? "0")}.${(match[2] ?? "").padEnd(2, "0")}`;
|
|
17
|
+
if (amount === "0.00")
|
|
18
|
+
return invalid("a payment's amount is more than 0");
|
|
19
|
+
return amount;
|
|
20
|
+
};
|
|
21
|
+
const dateOf = (value) => {
|
|
22
|
+
const date = typeof value === "string" ? value.trim() : "";
|
|
23
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date)
|
|
24
|
+
return invalid("a payment's due date is an ISO date, such as 2026-10-31");
|
|
25
|
+
return date;
|
|
26
|
+
};
|
|
27
|
+
const optionalText = (value, what, limit) => {
|
|
28
|
+
if (value === undefined || value === null)
|
|
29
|
+
return null;
|
|
30
|
+
if (typeof value !== "string" || !value.trim() || value.length > limit || value.includes("\0"))
|
|
31
|
+
return invalid(`a payment's ${what} takes 1–${limit} characters`);
|
|
32
|
+
return value.trim();
|
|
33
|
+
};
|
|
34
|
+
const oneOf = (value, allowed, what) => {
|
|
35
|
+
if (typeof value !== "string" || !allowed.includes(value))
|
|
36
|
+
return invalid(`a payment's ${what} is one of ${allowed.join(", ")}`);
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
const FIELDS = ["amount", "currency", "direction", "counterparty", "counterparty_name", "due_at", "state", "item"];
|
|
40
|
+
/** A payment as stored: the amount with two decimals, the currency upper case; refuses what is not one. */
|
|
41
|
+
export const validatePayment = (input) => {
|
|
42
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
43
|
+
return invalid("a payment is an object");
|
|
44
|
+
const raw = input;
|
|
45
|
+
const unknown = Object.keys(raw).filter((key) => !FIELDS.includes(key));
|
|
46
|
+
if (unknown.length)
|
|
47
|
+
return invalid(`a payment has no field ${unknown.join(", ")}; its fields are ${FIELDS.join(", ")}`);
|
|
48
|
+
const currency = typeof raw.currency === "string" ? raw.currency.trim().toUpperCase() : "";
|
|
49
|
+
if (!/^[A-Z]{3}$/.test(currency) || !CURRENCIES.has(currency))
|
|
50
|
+
return invalid("a payment's currency is an ISO 4217 code, such as EUR or RUB");
|
|
51
|
+
const counterparty = optionalText(raw.counterparty, "counterparty", 80);
|
|
52
|
+
if (counterparty !== null && !COUNTERPARTY.test(counterparty))
|
|
53
|
+
return invalid("a payment's counterparty is a person:, organization: or project: reference");
|
|
54
|
+
return {
|
|
55
|
+
amount: amountOf(raw.amount),
|
|
56
|
+
currency,
|
|
57
|
+
direction: oneOf(raw.direction, PAYMENT_DIRECTIONS, "direction"),
|
|
58
|
+
counterparty,
|
|
59
|
+
counterparty_name: optionalText(raw.counterparty_name, "counterparty name", 200),
|
|
60
|
+
due_at: raw.due_at === undefined || raw.due_at === null ? null : dateOf(raw.due_at),
|
|
61
|
+
state: oneOf(raw.state, PAYMENT_STATES, "state"),
|
|
62
|
+
item: optionalText(raw.item, "item", 200),
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const grouped = (amount) => {
|
|
66
|
+
const [whole = "0", cents = "00"] = amount.split(".");
|
|
67
|
+
const digits = whole.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
|
68
|
+
return cents === "00" ? digits : `${digits}.${cents}`;
|
|
69
|
+
};
|
|
70
|
+
/** One line for a payment, as a list or an inbox shows it: "1 200 EUR to us from Ana, due 31.10 (expected)". */
|
|
71
|
+
export const paymentTitle = (fact) => {
|
|
72
|
+
const payment = fact.payment;
|
|
73
|
+
if (!payment)
|
|
74
|
+
return "";
|
|
75
|
+
const who = payment.counterparty_name ?? payment.counterparty;
|
|
76
|
+
const way = payment.direction === "to_us" ? "to us" : "from us";
|
|
77
|
+
const party = who === null ? "" : payment.direction === "to_us" ? ` from ${who}` : ` to ${who}`;
|
|
78
|
+
const due = payment.due_at === null ? "" : `, due ${payment.due_at.slice(8, 10)}.${payment.due_at.slice(5, 7)}`;
|
|
79
|
+
const item = payment.item === null ? "" : ` for ${payment.item}`;
|
|
80
|
+
return `${grouped(payment.amount)} ${payment.currency} ${way}${party}${item}${due} (${payment.state})`;
|
|
81
|
+
};
|
|
@@ -6,6 +6,7 @@ import { type AccountKey, type ChatStats, type MessageStore, type Range } from "
|
|
|
6
6
|
import { type Estimate } from "./backfill-estimate.js";
|
|
7
7
|
import { batchProgress } from "./batch.js";
|
|
8
8
|
import type { ServiceDeps } from "./deps.js";
|
|
9
|
+
import { type ChatShape } from "./history-limits.js";
|
|
9
10
|
import { type CatchUpOptions, type CatchUpResult } from "./search-catchup.js";
|
|
10
11
|
/** The most messages a provider hands out per history request — Telegram's cap. */
|
|
11
12
|
export declare const PAGE = 100;
|
|
@@ -45,6 +46,10 @@ export interface FetchOptions {
|
|
|
45
46
|
done: number;
|
|
46
47
|
fetched: number;
|
|
47
48
|
}) => void;
|
|
49
|
+
/** Past the history limits for this chat (`--full-history`). */
|
|
50
|
+
fullHistory?: boolean;
|
|
51
|
+
/** The chat's kind and size as the messenger lists it, for a chat the store does not hold yet. */
|
|
52
|
+
shape?: ChatShape;
|
|
48
53
|
}
|
|
49
54
|
/** A type, not an interface: a job keeps it as a plain record. */
|
|
50
55
|
export type Fetched = {
|
|
@@ -62,6 +67,8 @@ export type Fetched = {
|
|
|
62
67
|
reachedSince?: true;
|
|
63
68
|
reachedLast?: true;
|
|
64
69
|
stopped?: true;
|
|
70
|
+
/** The history limits stopped this read: a group too large to read, or the window's start reached. */
|
|
71
|
+
limited?: "group_too_large" | "older_than_window";
|
|
65
72
|
};
|
|
66
73
|
export type FetchedAll = {
|
|
67
74
|
batch?: ReturnType<ReturnType<typeof batchProgress>["result"]>;
|
|
@@ -80,7 +87,13 @@ export type FetchedAll = {
|
|
|
80
87
|
stopped?: true;
|
|
81
88
|
error?: string;
|
|
82
89
|
issue?: ActionableError;
|
|
90
|
+
limited?: "group_too_large" | "older_than_window";
|
|
83
91
|
}[];
|
|
92
|
+
/** How many chats the history limits left unread (big groups) or read only to the window's start. */
|
|
93
|
+
limited?: {
|
|
94
|
+
groupTooLarge: number;
|
|
95
|
+
olderThanWindow: number;
|
|
96
|
+
};
|
|
84
97
|
};
|
|
85
98
|
/** The local store of messages: what it holds, filling it from the messenger, and reading it out. */
|
|
86
99
|
export interface ArchiveService {
|
package/dist/services/archive.js
CHANGED
|
@@ -5,6 +5,7 @@ import { typedCommand } from "../runs/scope.js";
|
|
|
5
5
|
import { fetchedKey, historyStartKey, } from "../store/store.js";
|
|
6
6
|
import { estimateBackfill } from "./backfill-estimate.js";
|
|
7
7
|
import { actionable, batchProgress } from "./batch.js";
|
|
8
|
+
import { historyWindow } from "./history-limits.js";
|
|
8
9
|
import { storedChatId } from "./message-resolution.js";
|
|
9
10
|
import { patiently } from "./patience.js";
|
|
10
11
|
import { catchUpSearch, validateCatchUp } from "./search-catchup.js";
|
|
@@ -114,13 +115,28 @@ export const archiveService = (deps) => {
|
|
|
114
115
|
throw new CliError("authentication_error", "the connection belongs to another account");
|
|
115
116
|
if (options.stop.aborted)
|
|
116
117
|
return { chat: null, fetched: 0, complete: false, ranges: [], stopped: true };
|
|
117
|
-
const
|
|
118
|
+
const store = await deps.store();
|
|
119
|
+
const account = { provider: deps.messenger.provider, account: self };
|
|
120
|
+
const limit = await limitOf(deps, store, account, chat, options);
|
|
121
|
+
if (limit?.skip) {
|
|
122
|
+
await recordSkip(store, account, limit.chatId, { reason: "group_too_large", members: limit.skip.members });
|
|
123
|
+
return { chat: limit.chatId ?? null, fetched: 0, complete: false, ranges: [], limited: "group_too_large" };
|
|
124
|
+
}
|
|
125
|
+
const windowed = limit?.keepFrom !== undefined && !(limit.keepFrom <= (options.sinceMs ?? Number.NEGATIVE_INFINITY));
|
|
126
|
+
const fetched = await fetchInto({
|
|
118
127
|
history: (window) => history(chat, { ...window, reactions: false }),
|
|
119
|
-
store
|
|
120
|
-
account
|
|
128
|
+
store,
|
|
129
|
+
account,
|
|
121
130
|
fetching: deps.messenger.fetching ?? FETCHING,
|
|
122
131
|
...options,
|
|
132
|
+
...(windowed ? { sinceMs: limit.keepFrom } : {}),
|
|
123
133
|
});
|
|
134
|
+
const result = windowed && fetched.reachedSince ? { ...fetched, limited: "older_than_window" } : fetched;
|
|
135
|
+
if (result.limited && limit?.keepFrom !== undefined)
|
|
136
|
+
await recordSkip(store, account, result.chat ?? limit.chatId, {
|
|
137
|
+
reason: "older_than_window",
|
|
138
|
+
keptFrom: new Date(limit.keepFrom).toISOString(),
|
|
139
|
+
});
|
|
124
140
|
return requested && result.chat
|
|
125
141
|
? { ...result, prepared: await catchUpSearch(deps, result.chat, requested, options.stop) }
|
|
126
142
|
: result;
|
|
@@ -161,7 +177,17 @@ export const archiveService = (deps) => {
|
|
|
161
177
|
continue;
|
|
162
178
|
}
|
|
163
179
|
try {
|
|
164
|
-
const one = await own.fetch(chat.id,
|
|
180
|
+
const one = await own.fetch(chat.id, {
|
|
181
|
+
...options,
|
|
182
|
+
shape: { kind: chat.kind, participantsCount: chat.participantsCount ?? null },
|
|
183
|
+
});
|
|
184
|
+
if (one.limited) {
|
|
185
|
+
answer.limited ??= { groupTooLarge: 0, olderThanWindow: 0 };
|
|
186
|
+
if (one.limited === "group_too_large")
|
|
187
|
+
answer.limited.groupTooLarge += 1;
|
|
188
|
+
else
|
|
189
|
+
answer.limited.olderThanWindow += 1;
|
|
190
|
+
}
|
|
165
191
|
if (one.issue)
|
|
166
192
|
batch.fail(chat.id, "history", one.issue);
|
|
167
193
|
else
|
|
@@ -172,7 +198,8 @@ export const archiveService = (deps) => {
|
|
|
172
198
|
title: chat.title,
|
|
173
199
|
fetched: one.fetched,
|
|
174
200
|
// Reaching the window is what --all asked for; older history was not.
|
|
175
|
-
complete: one.complete || one.reachedSince === true || one.reachedLast === true,
|
|
201
|
+
complete: one.complete || one.reachedSince === true || one.reachedLast === true || one.limited !== undefined,
|
|
202
|
+
...(one.limited ? { limited: one.limited } : {}),
|
|
176
203
|
...(one.issue ? { error: one.issue.code, issue: one.issue } : {}),
|
|
177
204
|
...(one.stopped ? { stopped: true } : {}),
|
|
178
205
|
});
|
|
@@ -195,6 +222,30 @@ export const archiveService = (deps) => {
|
|
|
195
222
|
},
|
|
196
223
|
};
|
|
197
224
|
};
|
|
225
|
+
/**
|
|
226
|
+
* Where the history limits stop this chat's read: not at all for a group above the cap, else at the window's start.
|
|
227
|
+
* None for a lifted chat or without limits. A chat the store does not hold yet is judged by the shape the list gave.
|
|
228
|
+
*/
|
|
229
|
+
const limitOf = async (deps, store, account, chat, options) => {
|
|
230
|
+
if (!deps.historyLimits || options.fullHistory)
|
|
231
|
+
return undefined;
|
|
232
|
+
const chatId = await storedChatId(deps.messenger, chat, store, account, { includeLeft: true }).catch(() => undefined);
|
|
233
|
+
const stored = chatId === undefined ? undefined : (await store.chats.historyShapes(account, [chatId])).get(chatId);
|
|
234
|
+
if (stored?.included)
|
|
235
|
+
return undefined;
|
|
236
|
+
const window = historyWindow(stored ?? options.shape, deps.historyLimits, Date.now());
|
|
237
|
+
return {
|
|
238
|
+
...(chatId === undefined ? {} : { chatId }),
|
|
239
|
+
...("skip" in window ? { skip: { members: window.members } } : { keepFrom: window.keepFrom }),
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
/** A read the limits stopped, for `store status` and the answers that say not all is there; the count is unknown. */
|
|
243
|
+
const recordSkip = async (store, account, chatId, skip) => {
|
|
244
|
+
if (!chatId)
|
|
245
|
+
return;
|
|
246
|
+
const at = new Date().toISOString();
|
|
247
|
+
await store.chats.recordHistorySkips(account, new Map([[chatId, { ...skip, messages: 0, oldestAt: at, newestAt: at, unread: true }]]));
|
|
248
|
+
};
|
|
198
249
|
/** The fetch loop, over any history and store: the personal account's service and a bot's command share it. */
|
|
199
250
|
export const abortable = async (read, signal) => {
|
|
200
251
|
signal.throwIfAborted();
|