@wirecat/cli-core 0.575.0 → 0.577.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 +37 -0
- package/dist/cli/findings-input.js +2 -0
- package/dist/cli/messenger/tasks-command.js +2 -0
- package/dist/domain/external-links.d.ts +13 -0
- package/dist/domain/external-links.js +30 -0
- package/dist/entities/index.d.ts +15 -0
- package/dist/entities/mail-findings.d.ts +6 -0
- package/dist/entities/mail-findings.js +6 -0
- package/dist/entities/task-results.d.ts +20 -0
- package/dist/entities/task-results.js +4 -0
- package/dist/entities/task.d.ts +4 -0
- package/dist/entities/task.js +4 -0
- package/dist/errors/reasons.js +5 -0
- package/dist/mail/cli/bulk-settings.d.ts +6 -0
- package/dist/mail/cli/bulk-settings.js +65 -0
- package/dist/mail/cli/index.d.ts +4 -0
- package/dist/mail/cli/index.js +3 -1
- package/dist/mail/cli/marks.d.ts +6 -0
- package/dist/mail/cli/marks.js +69 -1
- package/dist/mail/cli/review.js +34 -13
- package/dist/mail/findings.d.ts +2 -0
- package/dist/mail/findings.js +15 -1
- package/dist/mail/index.d.ts +2 -1
- package/dist/mail/index.js +2 -1
- package/dist/mail/marks.d.ts +48 -7
- package/dist/mail/marks.js +158 -16
- package/dist/mail/newsletter.d.ts +52 -0
- package/dist/mail/newsletter.js +189 -0
- package/dist/mail/review.d.ts +2 -0
- package/dist/meetings/findings-rules.d.ts +4 -0
- package/dist/store/sqlite/email-activity.d.ts +2 -0
- package/dist/store/sqlite/email-activity.js +4 -0
- package/dist/store/sqlite/findings-core.js +2 -0
- package/dist/store/sqlite/manifest.js +1 -0
- package/dist/store/sqlite/migrations.generated.js +10 -0
- package/dist/store/sqlite/orders.d.ts +4 -0
- package/dist/store/sqlite/orders.js +6 -3
- package/dist/store/sqlite/schema.d.ts +64 -0
- package/dist/store/sqlite/schema.js +5 -0
- package/dist/store/sqlite/work-tasks.d.ts +7 -0
- package/dist/store/sqlite/work-tasks.js +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.577.0 — 05.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Tasks keep their id in an outside service and the page they are about (store version 51).** `tasks.provider`
|
|
15
|
+
and `tasks.external_id` hold a task's id in a service such as Avito, Cian or hh.ru, given and shown as
|
|
16
|
+
`<service>:<id>` (`avito:4512345678`), so ids of two services never collide; several tasks may share one.
|
|
17
|
+
`tasks.source_url` and `orders.source_url` hold the http or https page a task or an order is about, such as an Avito
|
|
18
|
+
ad or an Amazon order; anything else is refused (`task.source-url`, `order.source-url`, `task.external-id`).
|
|
19
|
+
`TaskFields.externalId` and `sourceUrl` set them on create and edit, `null` clears them; `TaskDetails` and the
|
|
20
|
+
`task` entity carry `external_id` and `source_url`; `OrderInput` and `Order` carry `sourceUrl`. Mail findings take
|
|
21
|
+
`external_id` and `source_url` on a `task` and `source_url` on an `order`, and accepting keeps them. `minCompatible`
|
|
22
|
+
stays 34.
|
|
23
|
+
- **The owner moves a holder's mail mark back: `messages reset`.** `--holder <name>` with `--since-time <time>` or
|
|
24
|
+
`--last <n>`, and `--dry-run` to count first; the answer (`mail_mark_reset`) says how many emails the holder reads
|
|
25
|
+
whole again, how many bulk ones come again as excerpts and how many are skipped again. It is owner only: no
|
|
26
|
+
proposal, refused under the read-only lock, for a scheduled holder and for an agent (`mail.reset-owner-only`); a
|
|
27
|
+
holder with no mark yet is `mail.no-mark`. `rewindMark` is the function under it, and `MailActivityStore` gets
|
|
28
|
+
`emailsBefore`.
|
|
29
|
+
|
|
30
|
+
## 0.576.0 — 05.11.2026
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **`review --holder` gives an agent the bulk mail that is not surely a newsletter.** Shops, banks and site forms put
|
|
35
|
+
List-Unsubscribe on orders, invoices and lead forms too, and the holder read left all of it out. Now
|
|
36
|
+
`newsletterSorter` scores each bulk email by words in its subject and first lines (Russian, English, Spanish), its
|
|
37
|
+
sender's address, an emoji or an unsubscribe link: a newsletter is left out and counted under `missing` as
|
|
38
|
+
`newsletter`; an order word in the subject, an order's or a request's number, or a reply keeps it. A kept email
|
|
39
|
+
comes after the other mail, as its subject and first lines, with `email.bulk: true` and `email.excerpt_only: true`;
|
|
40
|
+
`messages show <ref>` reads it whole. At most 30 excerpts a read; the read then ends early and the next gives the
|
|
41
|
+
rest. `mailWaiting` and `advancePastSkipped` count and stop at excerpts the same way. A host sets it from its config
|
|
42
|
+
file: `MAIL_BULK_SETTINGS` (`mail_bulk_excerpts`, `mail_bulk_excerpts_per_read`, `mail_bulk_excerpt_chars`,
|
|
43
|
+
`mail_bulk_newsletter_score`, `mail_bulk_newsletter_words`, `mail_bulk_newsletter_senders`, `mail_bulk_order_words`),
|
|
44
|
+
read by `bulkTriageOf` into `MailReads.bulkTriage`, and passed to `mailWaiting` and `advancePastSkipped` as their
|
|
45
|
+
new last argument; without it the defaults apply.
|
|
46
|
+
|
|
10
47
|
## 0.575.0 — 05.11.2026
|
|
11
48
|
|
|
12
49
|
### Added
|
|
@@ -14,6 +14,8 @@ const keys = {
|
|
|
14
14
|
supplier_name: "supplierName",
|
|
15
15
|
order_date: "orderDate",
|
|
16
16
|
fulfilled_date: "fulfilledDate",
|
|
17
|
+
external_id: "externalId",
|
|
18
|
+
source_url: "sourceUrl",
|
|
17
19
|
};
|
|
18
20
|
const parseFindingsJson = (text, label, validate) => {
|
|
19
21
|
if (Buffer.byteLength(text, "utf8") > 65536)
|
|
@@ -41,6 +41,8 @@ const card = (task) => [
|
|
|
41
41
|
`due ${when(task.dueAt)} next check ${when(task.nextCheckAt)} source ${task.source ?? "—"}`,
|
|
42
42
|
...(task.priorityLabel === null ? [] : [`priority ${task.priorityLabel}`]),
|
|
43
43
|
...(task.agent === null ? [] : [`agent ${task.agent}`]),
|
|
44
|
+
...(task.externalId === null ? [] : [`external id ${task.externalId}`]),
|
|
45
|
+
...(task.sourceUrl === null ? [] : [`page ${task.sourceUrl}`]),
|
|
44
46
|
...(task.claimedBy === null ? [] : [`claimed by ${task.claimedBy} until ${when(task.claimedUntil)}`]),
|
|
45
47
|
...task.assignments.filter((one) => one.role === "assignee").map((one) => `assignee ${one.to}`),
|
|
46
48
|
...(task.closeSuggestion === null ? [] : [suggestionLine(task.closeSuggestion)]),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The outside service an external id belongs to: `avito`, `cian`, `amazon`, `hh`. */
|
|
2
|
+
export declare const EXTERNAL_SERVICE: RegExp;
|
|
3
|
+
export declare const EXTERNAL_ID_CHARS = 200;
|
|
4
|
+
export declare const SOURCE_URL_CHARS = 2000;
|
|
5
|
+
export interface ExternalId {
|
|
6
|
+
provider: string;
|
|
7
|
+
id: string;
|
|
8
|
+
}
|
|
9
|
+
/** `avito:4512345678` → the service and its id; the id is everything after the first colon. */
|
|
10
|
+
export declare const externalIdOf: (value: string) => ExternalId;
|
|
11
|
+
export declare const externalIdText: (provider: unknown, id: unknown) => string | null;
|
|
12
|
+
/** An http or https address, as given; anything else is refused, so a stored link always opens a web page. */
|
|
13
|
+
export declare const sourceUrlOf: (value: string, reason?: string) => string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
/** The outside service an external id belongs to: `avito`, `cian`, `amazon`, `hh`. */
|
|
3
|
+
export const EXTERNAL_SERVICE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
4
|
+
export const EXTERNAL_ID_CHARS = 200;
|
|
5
|
+
export const SOURCE_URL_CHARS = 2000;
|
|
6
|
+
/** `avito:4512345678` → the service and its id; the id is everything after the first colon. */
|
|
7
|
+
export const externalIdOf = (value) => {
|
|
8
|
+
const text = value.trim();
|
|
9
|
+
const colon = text.indexOf(":");
|
|
10
|
+
const provider = colon === -1 ? "" : text.slice(0, colon).toLowerCase();
|
|
11
|
+
const id = colon === -1 ? "" : text.slice(colon + 1).trim();
|
|
12
|
+
if (!EXTERNAL_SERVICE.test(provider) || id.length === 0 || id.length > EXTERNAL_ID_CHARS || /\s/.test(id))
|
|
13
|
+
throw new CliError("validation_error", `an external id is <service>:<id>, such as avito:4512345678, at most ${EXTERNAL_ID_CHARS} characters after the colon and no spaces, not "${value}"`, { reason: "task.external-id" });
|
|
14
|
+
return { provider, id };
|
|
15
|
+
};
|
|
16
|
+
export const externalIdText = (provider, id) => provider == null || id == null ? null : `${String(provider)}:${String(id)}`;
|
|
17
|
+
/** An http or https address, as given; anything else is refused, so a stored link always opens a web page. */
|
|
18
|
+
export const sourceUrlOf = (value, reason = "task.source-url") => {
|
|
19
|
+
const text = value.trim();
|
|
20
|
+
let url;
|
|
21
|
+
try {
|
|
22
|
+
url = new URL(text);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
url = undefined;
|
|
26
|
+
}
|
|
27
|
+
if (!url || (url.protocol !== "http:" && url.protocol !== "https:") || text.length > SOURCE_URL_CHARS)
|
|
28
|
+
throw new CliError("validation_error", `a source URL is an http or https address of at most ${SOURCE_URL_CHARS} characters, not "${value}"`, { reason });
|
|
29
|
+
return text;
|
|
30
|
+
};
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -4303,6 +4303,8 @@ export declare const ENTITIES: {
|
|
|
4303
4303
|
readonly account: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
4304
4304
|
readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
4305
4305
|
readonly source_message: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
4306
|
+
readonly external_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4307
|
+
readonly source_url: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
4306
4308
|
readonly priority: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
4307
4309
|
readonly priority_label: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>, undefined>;
|
|
4308
4310
|
readonly agent: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
@@ -4386,6 +4388,8 @@ export declare const ENTITIES: {
|
|
|
4386
4388
|
readonly revision: v.NumberSchema<undefined>;
|
|
4387
4389
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
4388
4390
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4391
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4392
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4389
4393
|
readonly due_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>;
|
|
4390
4394
|
readonly next_check_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>;
|
|
4391
4395
|
readonly closed_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>;
|
|
@@ -4500,6 +4504,8 @@ export declare const ENTITIES: {
|
|
|
4500
4504
|
readonly revision: v.NumberSchema<undefined>;
|
|
4501
4505
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
4502
4506
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4507
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4508
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4503
4509
|
readonly due_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>;
|
|
4504
4510
|
readonly next_check_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>;
|
|
4505
4511
|
readonly closed_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>;
|
|
@@ -4615,6 +4621,8 @@ export declare const ENTITIES: {
|
|
|
4615
4621
|
readonly revision: v.NumberSchema<undefined>;
|
|
4616
4622
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
4617
4623
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4624
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4625
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4618
4626
|
readonly due_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>;
|
|
4619
4627
|
readonly next_check_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>;
|
|
4620
4628
|
readonly closed_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>;
|
|
@@ -4758,6 +4766,8 @@ export declare const ENTITIES: {
|
|
|
4758
4766
|
readonly revision: v.NumberSchema<undefined>;
|
|
4759
4767
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
4760
4768
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4769
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4770
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4761
4771
|
readonly due_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>;
|
|
4762
4772
|
readonly next_check_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>;
|
|
4763
4773
|
readonly closed_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>;
|
|
@@ -4866,6 +4876,8 @@ export declare const ENTITIES: {
|
|
|
4866
4876
|
readonly revision: v.NumberSchema<undefined>;
|
|
4867
4877
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
4868
4878
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4879
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4880
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
4869
4881
|
readonly due_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>;
|
|
4870
4882
|
readonly next_check_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>;
|
|
4871
4883
|
readonly closed_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>;
|
|
@@ -5911,6 +5923,8 @@ export declare const ENTITIES: {
|
|
|
5911
5923
|
readonly due_evidence: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
5912
5924
|
readonly assignee: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
5913
5925
|
readonly assignee_evidence: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
5926
|
+
readonly external_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
5927
|
+
readonly source_url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
5914
5928
|
}, undefined>, v.StrictObjectSchema<{
|
|
5915
5929
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
5916
5930
|
readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -6028,6 +6042,7 @@ export declare const ENTITIES: {
|
|
|
6028
6042
|
readonly invoice: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
6029
6043
|
readonly payment: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
6030
6044
|
readonly project: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
6045
|
+
readonly source_url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
6031
6046
|
readonly reason: v.StringSchema<undefined>;
|
|
6032
6047
|
}, undefined>, v.StrictObjectSchema<{
|
|
6033
6048
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -26,6 +26,10 @@ export declare const mailFindings: v.StrictObjectSchema<{
|
|
|
26
26
|
readonly due_evidence: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
27
27
|
readonly assignee: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
28
28
|
readonly assignee_evidence: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
29
|
+
/** `<service>:<id>` in the outside service the mail came from, such as `avito:4512345678`. */
|
|
30
|
+
readonly external_id: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
31
|
+
/** The http or https page the task is about, such as the conversation the mail links to. */
|
|
32
|
+
readonly source_url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
29
33
|
}, undefined>, v.StrictObjectSchema<{
|
|
30
34
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
31
35
|
readonly evidence: v.ArraySchema<v.StrictObjectSchema<{
|
|
@@ -143,6 +147,8 @@ export declare const mailFindings: v.StrictObjectSchema<{
|
|
|
143
147
|
readonly invoice: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
144
148
|
readonly payment: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
145
149
|
readonly project: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
150
|
+
/** The order's page at the shop, http or https. */
|
|
151
|
+
readonly source_url: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
146
152
|
readonly reason: v.StringSchema<undefined>;
|
|
147
153
|
}, undefined>, v.StrictObjectSchema<{
|
|
148
154
|
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -18,6 +18,10 @@ const finding = v.variant("kind", [
|
|
|
18
18
|
due_evidence: v.optional(v.array(reference)),
|
|
19
19
|
assignee: v.optional(reference),
|
|
20
20
|
assignee_evidence: v.optional(v.array(reference)),
|
|
21
|
+
/** `<service>:<id>` in the outside service the mail came from, such as `avito:4512345678`. */
|
|
22
|
+
external_id: v.optional(text),
|
|
23
|
+
/** The http or https page the task is about, such as the conversation the mail links to. */
|
|
24
|
+
source_url: v.optional(text),
|
|
21
25
|
}),
|
|
22
26
|
v.strictObject({
|
|
23
27
|
...base,
|
|
@@ -96,6 +100,8 @@ const finding = v.variant("kind", [
|
|
|
96
100
|
invoice: v.optional(reference),
|
|
97
101
|
payment: v.optional(reference),
|
|
98
102
|
project: v.optional(reference),
|
|
103
|
+
/** The order's page at the shop, http or https. */
|
|
104
|
+
source_url: v.optional(text),
|
|
99
105
|
reason: text,
|
|
100
106
|
}),
|
|
101
107
|
v.strictObject({
|
|
@@ -22,6 +22,10 @@ export declare const taskCard: v.StrictObjectSchema<{
|
|
|
22
22
|
readonly revision: v.NumberSchema<undefined>;
|
|
23
23
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
24
24
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
25
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
26
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
27
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
28
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
25
29
|
readonly due_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>;
|
|
26
30
|
readonly next_check_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>;
|
|
27
31
|
readonly closed_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>;
|
|
@@ -136,6 +140,10 @@ export declare const taskCreated: v.StrictObjectSchema<{
|
|
|
136
140
|
readonly revision: v.NumberSchema<undefined>;
|
|
137
141
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
138
142
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
143
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
144
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
145
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
146
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
139
147
|
readonly due_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>;
|
|
140
148
|
readonly next_check_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>;
|
|
141
149
|
readonly closed_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>;
|
|
@@ -251,6 +259,10 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
251
259
|
readonly revision: v.NumberSchema<undefined>;
|
|
252
260
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
253
261
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
262
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
263
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
264
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
265
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
254
266
|
readonly due_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>;
|
|
255
267
|
readonly next_check_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>;
|
|
256
268
|
readonly closed_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>;
|
|
@@ -394,6 +406,10 @@ export declare const taskMerge: v.StrictObjectSchema<{
|
|
|
394
406
|
readonly revision: v.NumberSchema<undefined>;
|
|
395
407
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
396
408
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
409
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
410
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
411
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
412
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
397
413
|
readonly due_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>;
|
|
398
414
|
readonly next_check_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>;
|
|
399
415
|
readonly closed_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>;
|
|
@@ -502,6 +518,10 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
|
|
|
502
518
|
readonly revision: v.NumberSchema<undefined>;
|
|
503
519
|
readonly parent: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
|
|
504
520
|
readonly source: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
521
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
522
|
+
readonly external_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
523
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
524
|
+
readonly source_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
505
525
|
readonly due_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>;
|
|
506
526
|
readonly next_check_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>;
|
|
507
527
|
readonly closed_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>;
|
|
@@ -39,6 +39,10 @@ export const taskCard = v.strictObject({
|
|
|
39
39
|
revision: v.number(),
|
|
40
40
|
parent: v.nullable(reference),
|
|
41
41
|
source: v.nullable(text),
|
|
42
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
43
|
+
external_id: v.nullable(text),
|
|
44
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
45
|
+
source_url: v.nullable(text),
|
|
42
46
|
due_at: nullableTime,
|
|
43
47
|
next_check_at: nullableTime,
|
|
44
48
|
closed_at: nullableTime,
|
package/dist/entities/task.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ export declare const task: v.StrictObjectSchema<{
|
|
|
34
34
|
readonly chat: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
35
35
|
/** The message it came from. */
|
|
36
36
|
readonly source_message: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>, undefined>;
|
|
37
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
38
|
+
readonly external_id: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
39
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
40
|
+
readonly source_url: v.OptionalSchema<v.NullableSchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
37
41
|
/** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
|
|
38
42
|
readonly priority: v.OptionalSchema<v.NullableSchema<v.NumberSchema<undefined>, undefined>, undefined>;
|
|
39
43
|
readonly priority_label: v.OptionalSchema<v.NullableSchema<v.PicklistSchema<readonly ["low", "medium", "high", "critical"], undefined>, undefined>, undefined>;
|
package/dist/entities/task.js
CHANGED
|
@@ -32,6 +32,10 @@ export const task = v.strictObject({
|
|
|
32
32
|
chat: v.optional(v.nullable(reference)),
|
|
33
33
|
/** The message it came from. */
|
|
34
34
|
source_message: v.optional(v.nullable(reference)),
|
|
35
|
+
/** `<service>:<id>`, its id in an outside service, such as `avito:4512345678`. */
|
|
36
|
+
external_id: v.optional(v.nullable(text)),
|
|
37
|
+
/** The http or https page it is about, such as an Avito ad. */
|
|
38
|
+
source_url: v.optional(v.nullable(text)),
|
|
35
39
|
/** 0 to 1, higher first; `priority_label` reads it as low, medium, high or critical. */
|
|
36
40
|
priority: v.optional(v.nullable(v.number())),
|
|
37
41
|
priority_label: v.optional(v.nullable(v.picklist(PRIORITY_LABELS))),
|
package/dist/errors/reasons.js
CHANGED
|
@@ -69,6 +69,8 @@ const OTHERS = [
|
|
|
69
69
|
validation("file.locked", "another process holds the file's lock"),
|
|
70
70
|
validation("mail.cursor-ahead", "an acknowledge cursor is past the newest stored email"),
|
|
71
71
|
["mail.holder-not-yours", "permission_error", "a run moves only the mark of the holder its schedule names"],
|
|
72
|
+
["mail.no-mark", "not_found", "the holder has no mark in the mailbox yet; its first review starts it"],
|
|
73
|
+
["mail.reset-owner-only", "permission_error", "only the owner moves a holder's mark back"],
|
|
72
74
|
["mail.source-failed", "provider_error", "the mail server refused or did not complete a request"],
|
|
73
75
|
["mail.search-refused", "provider_error", "the mail server refused to search a folder"],
|
|
74
76
|
[
|
|
@@ -199,6 +201,7 @@ const OTHERS = [
|
|
|
199
201
|
validation("money.quantity", "the quantity is not a decimal above 0"),
|
|
200
202
|
validation("order.duplicate", "the shop's order with this number is already stored; change its status instead"),
|
|
201
203
|
validation("order.field", "a field of the order is not valid"),
|
|
204
|
+
validation("order.source-url", "an order's source URL is not an http or https address"),
|
|
202
205
|
["order.not-found", "not_found", "no stored order of that shop has that number"],
|
|
203
206
|
validation("order.ref", "the reference names no order, or a shop's number names more than one"),
|
|
204
207
|
validation("order.status", "a rejected or deleted order's status does not change"),
|
|
@@ -246,6 +249,8 @@ const OTHERS = [
|
|
|
246
249
|
validation("tag.invalid", "the tag is not valid"),
|
|
247
250
|
["reactions.list-hidden", "permission_error", "the chat does not let its members see who reacted"],
|
|
248
251
|
["reactions.broadcast", "permission_error", "a channel does not show who reacted to its posts"],
|
|
252
|
+
validation("task.external-id", "an external id is <service>:<id>, such as avito:4512345678"),
|
|
253
|
+
validation("task.source-url", "a task's source URL is not an http or https address"),
|
|
249
254
|
validation("task.category-unknown", "the category is not in the task category list"),
|
|
250
255
|
validation("task.claimed", "someone else holds the task"),
|
|
251
256
|
validation("task.closed", "the task is already closed"),
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DeclaredSetting } from "../../cli/setting-defaults.js";
|
|
2
|
+
import { type BulkTriage } from "../newsletter.js";
|
|
3
|
+
/** The bulk-mail settings a host that mounts `review --holder` writes into its config file's top level (WC-CMD-30). */
|
|
4
|
+
export declare const MAIL_BULK_SETTINGS: readonly DeclaredSetting[];
|
|
5
|
+
/** `MAIL_BULK_SETTINGS` as the config file gives them; a missing or unusable value is left to the default. */
|
|
6
|
+
export declare const bulkTriageOf: (config: Record<string, unknown>) => BulkTriage;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BULK_TRIAGE_DEFAULTS, NEWSLETTER_DEFAULTS, NEWSLETTER_SENDERS, NEWSLETTER_WORDS, ORDER_WORDS, } from "../newsletter.js";
|
|
2
|
+
const listed = (words) => words.join(", ");
|
|
3
|
+
/** The bulk-mail settings a host that mounts `review --holder` writes into its config file's top level (WC-CMD-30). */
|
|
4
|
+
export const MAIL_BULK_SETTINGS = [
|
|
5
|
+
{
|
|
6
|
+
key: "mail_bulk_excerpts",
|
|
7
|
+
default: BULK_TRIAGE_DEFAULTS.excerpts,
|
|
8
|
+
description: "review --holder gives an agent bulk mail that is not surely a newsletter as its subject and first lines; " +
|
|
9
|
+
"false leaves all bulk mail out",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
key: "mail_bulk_excerpts_per_read",
|
|
13
|
+
default: BULK_TRIAGE_DEFAULTS.perRead,
|
|
14
|
+
description: "the most bulk excerpts one review --holder gives; the rest come with the next read",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "mail_bulk_excerpt_chars",
|
|
18
|
+
default: NEWSLETTER_DEFAULTS.excerptChars,
|
|
19
|
+
description: "how many characters of a bulk email's own text an excerpt shows",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "mail_bulk_newsletter_score",
|
|
23
|
+
default: NEWSLETTER_DEFAULTS.threshold,
|
|
24
|
+
description: "bulk mail scoring this or more is left out as a newsletter: a newsletter word counts 2 in the subject, 1 in " +
|
|
25
|
+
"the first lines, a newsletter's address 2, an emoji in the subject 1, an unsubscribe link at the top 1, " +
|
|
26
|
+
"an order word in the first lines -2",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "mail_bulk_newsletter_words",
|
|
30
|
+
default: listed(NEWSLETTER_WORDS),
|
|
31
|
+
description: "comma-separated words that say advertising; each matches at the start of a word, any case",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "mail_bulk_newsletter_senders",
|
|
35
|
+
default: listed(NEWSLETTER_SENDERS),
|
|
36
|
+
description: "comma-separated parts of a sender's address (news, digest) or domains (substack.com) of newsletters",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "mail_bulk_order_words",
|
|
40
|
+
default: listed(ORDER_WORDS),
|
|
41
|
+
description: "comma-separated words of orders, invoices and requests: one in the subject keeps a bulk email for the agent",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
const wordsOf = (value) => typeof value === "string"
|
|
45
|
+
? value
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((word) => word.trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
: undefined;
|
|
50
|
+
const countOf = (value, least) => typeof value === "number" && Number.isSafeInteger(value) && value >= least ? value : undefined;
|
|
51
|
+
/** `MAIL_BULK_SETTINGS` as the config file gives them; a missing or unusable value is left to the default. */
|
|
52
|
+
export const bulkTriageOf = (config) => {
|
|
53
|
+
const { cues } = BULK_TRIAGE_DEFAULTS;
|
|
54
|
+
return {
|
|
55
|
+
excerpts: typeof config.mail_bulk_excerpts === "boolean" ? config.mail_bulk_excerpts : BULK_TRIAGE_DEFAULTS.excerpts,
|
|
56
|
+
perRead: countOf(config.mail_bulk_excerpts_per_read, 1) ?? BULK_TRIAGE_DEFAULTS.perRead,
|
|
57
|
+
cues: {
|
|
58
|
+
threshold: countOf(config.mail_bulk_newsletter_score, 1) ?? cues.threshold,
|
|
59
|
+
excerptChars: countOf(config.mail_bulk_excerpt_chars, 40) ?? cues.excerptChars,
|
|
60
|
+
newsletterWords: wordsOf(config.mail_bulk_newsletter_words) ?? cues.newsletterWords,
|
|
61
|
+
newsletterSenders: wordsOf(config.mail_bulk_newsletter_senders) ?? cues.newsletterSenders,
|
|
62
|
+
orderWords: wordsOf(config.mail_bulk_order_words) ?? cues.orderWords,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
package/dist/mail/cli/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
+
import type { BulkTriage } from "../../mail/newsletter.js";
|
|
2
3
|
import { type MailServerAccess } from "../../mail/server-search.js";
|
|
3
4
|
import type { AppIdentity } from "../../ports/app.js";
|
|
4
5
|
import { type AccountKey, type MessageStore } from "../../store/store.js";
|
|
6
|
+
export { bulkTriageOf, MAIL_BULK_SETTINGS } from "./bulk-settings.js";
|
|
5
7
|
export { mailMark, markItem, markText } from "./marks.js";
|
|
6
8
|
export { MAIL_REVIEW_QUERY, MAIL_REVIEW_STEPS, mailReviewSteps } from "./review.js";
|
|
7
9
|
/**
|
|
@@ -37,6 +39,8 @@ export interface MailReads {
|
|
|
37
39
|
* With it, `messages show` reads an email whose attachment a sync's policy left unread before it answers.
|
|
38
40
|
*/
|
|
39
41
|
whole?: (command: Command, store: MessageStore, mailbox: AccountKey, emailId: string) => Promise<void>;
|
|
42
|
+
/** How `review --holder` treats bulk mail, from the host's config file (`MAIL_BULK_SETTINGS`); defaults without it. */
|
|
43
|
+
bulkTriage?: (command: Command) => BulkTriage;
|
|
40
44
|
}
|
|
41
45
|
export declare const THREAD_QUERY: {
|
|
42
46
|
readonly entity: "mail_thread";
|
package/dist/mail/cli/index.js
CHANGED
|
@@ -19,8 +19,9 @@ import { searchMailServer } from "../../mail/server-search.js";
|
|
|
19
19
|
import { atDetail } from "../../output-detail.js";
|
|
20
20
|
import { sourceKey } from "../../services/server-search.js";
|
|
21
21
|
import { queryFilter, THREAD_COLUMNS } from "../../store/store.js";
|
|
22
|
-
import { acknowledgeCommand } from "./marks.js";
|
|
22
|
+
import { acknowledgeCommand, resetMarkCommand } from "./marks.js";
|
|
23
23
|
import { mailReviewCommand } from "./review.js";
|
|
24
|
+
export { bulkTriageOf, MAIL_BULK_SETTINGS } from "./bulk-settings.js";
|
|
24
25
|
export { mailMark, markItem, markText } from "./marks.js";
|
|
25
26
|
export { MAIL_REVIEW_QUERY, MAIL_REVIEW_STEPS, mailReviewSteps } from "./review.js";
|
|
26
27
|
/** A locked or broken store must fail the command, not read as "nothing stored yet". */
|
|
@@ -253,6 +254,7 @@ const messagesCommand = (mail) => {
|
|
|
253
254
|
: itemOf(shown.find((_, at) => emails[at]?.anchor) ?? shown[0], unread ? { meta: { complete: false, issues: [unread] } } : undefined), () => shownText(emails));
|
|
254
255
|
});
|
|
255
256
|
messages.addCommand(acknowledgeCommand(mail));
|
|
257
|
+
messages.addCommand(resetMarkCommand(mail));
|
|
256
258
|
return messages;
|
|
257
259
|
};
|
|
258
260
|
const mailThread = v.strictObject({
|
package/dist/mail/cli/marks.d.ts
CHANGED
|
@@ -26,3 +26,9 @@ export declare const markText: (mark: MailMark) => string;
|
|
|
26
26
|
* schedule names only: an email that talks its reader into acknowledging would otherwise silence another pipeline.
|
|
27
27
|
*/
|
|
28
28
|
export declare const acknowledgeCommand: (mail: MailReads) => Command;
|
|
29
|
+
/**
|
|
30
|
+
* `messages reset`: the owner moves a holder's mark back, so its next reviews give it that mail again — after its
|
|
31
|
+
* rules changed, say. An agent never does: a holder that could move its own mark back could be talked by an email
|
|
32
|
+
* into reading the whole mailbox again, so this is no proposal and the read-only lock refuses it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const resetMarkCommand: (mail: MailReads) => Command;
|
package/dist/mail/cli/marks.js
CHANGED
|
@@ -8,7 +8,8 @@ import { environmentOf, outputFor } from "../../cli/context.js";
|
|
|
8
8
|
import { LOCAL_WRITE, proposesOnly } from "../../cli/effects.js";
|
|
9
9
|
import { itemOf, renderAnswer } from "../../cli/envelope.js";
|
|
10
10
|
import { mailHolderVariable, readOnlyLockOf } from "../../ports/app.js";
|
|
11
|
-
import {
|
|
11
|
+
import { momentOf } from "../../services/moment.js";
|
|
12
|
+
import { acknowledgeMark, rewindMark } from "../marks.js";
|
|
12
13
|
const iso = (ms) => (ms === null ? null : new Date(ms).toISOString());
|
|
13
14
|
/** A holder's mark as `--json`, `mail status` and MCP show it. */
|
|
14
15
|
export const mailMark = v.strictObject({
|
|
@@ -67,3 +68,70 @@ export const acknowledgeCommand = (mail) => answerEntity(proposesOnly(annotate(n
|
|
|
67
68
|
const mark = await mail.read(this, (store, mailbox) => acknowledgeMark(store, mailbox, holder, cursor));
|
|
68
69
|
renderAnswer(this, outputFor(this), itemOf(markItem(mark)), () => markText(mark));
|
|
69
70
|
});
|
|
71
|
+
const mailMarkReset = v.strictObject({
|
|
72
|
+
mark: mailMark,
|
|
73
|
+
/** Where the mark was before. */
|
|
74
|
+
from_id: v.number(),
|
|
75
|
+
/** Emails the holder reads whole again. */
|
|
76
|
+
again: v.number(),
|
|
77
|
+
/** Bulk emails it gets again as excerpts, unless they are surely newsletters. */
|
|
78
|
+
bulk: v.number(),
|
|
79
|
+
/** Emails between the two marks it skips again: drafts, envelopes, suspicious senders, partial mail. */
|
|
80
|
+
skipped: v.number(),
|
|
81
|
+
/** Nothing moved: `--dry-run` only counts. */
|
|
82
|
+
dry_run: v.boolean(),
|
|
83
|
+
});
|
|
84
|
+
const wholeNumber = (value) => {
|
|
85
|
+
const parsed = Number(value);
|
|
86
|
+
if (!Number.isSafeInteger(parsed) || parsed < 1)
|
|
87
|
+
throw new CliError("validation_error", `--last takes a whole number of emails, 1 or more — got ${value}`);
|
|
88
|
+
return parsed;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* `messages reset`: the owner moves a holder's mark back, so its next reviews give it that mail again — after its
|
|
92
|
+
* rules changed, say. An agent never does: a holder that could move its own mark back could be talked by an email
|
|
93
|
+
* into reading the whole mailbox again, so this is no proposal and the read-only lock refuses it.
|
|
94
|
+
*/
|
|
95
|
+
export const resetMarkCommand = (mail) => answerEntity(annotate(new Command("reset"), {
|
|
96
|
+
effects: LOCAL_WRITE,
|
|
97
|
+
examples: [
|
|
98
|
+
`${mail.app.command} personal messages reset --holder mail-triage --last 8 --dry-run`,
|
|
99
|
+
`${mail.app.command} personal messages reset --holder mail-triage --since-time 2026-11-04T00:00:00Z`,
|
|
100
|
+
],
|
|
101
|
+
}), "mail_mark_reset", mailMarkReset)
|
|
102
|
+
.summary("move a holder's processing mark back, so it reads that mail again (owner only)")
|
|
103
|
+
.description("the owner moves a holder's mark back past the mail received since --since-time, or past the last --last " +
|
|
104
|
+
"emails it was given; its next reviews give that mail again. The answer says how many emails it reads " +
|
|
105
|
+
"again. An agent cannot: a holder only moves its own mark forward, with `messages acknowledge`")
|
|
106
|
+
.requiredOption("--holder <name>", "whose mark: lower-case letters, digits and dashes, such as mail-triage")
|
|
107
|
+
.option("--since-time <time>", "read again what was received since this ISO 8601 time, or 2d ago")
|
|
108
|
+
.option("--last <n>", "read again the last n emails the holder was given", wholeNumber)
|
|
109
|
+
.option("--dry-run", "count what would be read again, and move nothing")
|
|
110
|
+
.action(async function () {
|
|
111
|
+
const { holder, sinceTime, last, dryRun } = this.opts();
|
|
112
|
+
if ((sinceTime === undefined) === (last === undefined))
|
|
113
|
+
throw new CliError("validation_error", "give --since-time or --last, one of them");
|
|
114
|
+
const env = environmentOf(this).env ?? process.env;
|
|
115
|
+
if (env[mailHolderVariable(mail.app)]?.trim() || actorOf(this) !== "owner" || readOnlyLockOf(mail.app, env))
|
|
116
|
+
throw new CliError("permission_error", "only the owner moves a holder's mark back; an agent only acknowledges", {
|
|
117
|
+
reason: "mail.reset-owner-only",
|
|
118
|
+
});
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const to = last === undefined ? { since: momentOf(sinceTime, "--since-time", now) } : { last };
|
|
121
|
+
const reset = await mail.read(this, (store, mailbox) => rewindMark(store, mailbox, holder, to, { now, dry: dryRun === true }));
|
|
122
|
+
const item = {
|
|
123
|
+
mark: markItem(reset.mark),
|
|
124
|
+
from_id: reset.fromId,
|
|
125
|
+
again: reset.again,
|
|
126
|
+
bulk: reset.bulk,
|
|
127
|
+
skipped: reset.skipped,
|
|
128
|
+
dry_run: reset.dry,
|
|
129
|
+
};
|
|
130
|
+
renderAnswer(this, outputFor(this), itemOf(item), () => {
|
|
131
|
+
const mark = reset.mark.throughId === reset.fromId
|
|
132
|
+
? `the mark stays at ${reset.fromId}`
|
|
133
|
+
: `the mark ${reset.dry ? "would move" : "moved"} from ${reset.fromId} to ${reset.mark.throughId}`;
|
|
134
|
+
return (`${holder} ${reset.dry ? "would read" : "reads"} ${reset.again} emails again, and ${reset.bulk} bulk ` +
|
|
135
|
+
`ones as excerpts unless they are newsletters; ${reset.skipped} are skipped again; ${mark}\n`);
|
|
136
|
+
});
|
|
137
|
+
});
|