@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cli/findings-input.js +2 -0
  3. package/dist/cli/messenger/tasks-command.js +2 -0
  4. package/dist/domain/external-links.d.ts +13 -0
  5. package/dist/domain/external-links.js +30 -0
  6. package/dist/entities/index.d.ts +15 -0
  7. package/dist/entities/mail-findings.d.ts +6 -0
  8. package/dist/entities/mail-findings.js +6 -0
  9. package/dist/entities/task-results.d.ts +20 -0
  10. package/dist/entities/task-results.js +4 -0
  11. package/dist/entities/task.d.ts +4 -0
  12. package/dist/entities/task.js +4 -0
  13. package/dist/errors/reasons.js +5 -0
  14. package/dist/mail/cli/bulk-settings.d.ts +6 -0
  15. package/dist/mail/cli/bulk-settings.js +65 -0
  16. package/dist/mail/cli/index.d.ts +4 -0
  17. package/dist/mail/cli/index.js +3 -1
  18. package/dist/mail/cli/marks.d.ts +6 -0
  19. package/dist/mail/cli/marks.js +69 -1
  20. package/dist/mail/cli/review.js +34 -13
  21. package/dist/mail/findings.d.ts +2 -0
  22. package/dist/mail/findings.js +15 -1
  23. package/dist/mail/index.d.ts +2 -1
  24. package/dist/mail/index.js +2 -1
  25. package/dist/mail/marks.d.ts +48 -7
  26. package/dist/mail/marks.js +158 -16
  27. package/dist/mail/newsletter.d.ts +52 -0
  28. package/dist/mail/newsletter.js +189 -0
  29. package/dist/mail/review.d.ts +2 -0
  30. package/dist/meetings/findings-rules.d.ts +4 -0
  31. package/dist/store/sqlite/email-activity.d.ts +2 -0
  32. package/dist/store/sqlite/email-activity.js +4 -0
  33. package/dist/store/sqlite/findings-core.js +2 -0
  34. package/dist/store/sqlite/manifest.js +1 -0
  35. package/dist/store/sqlite/migrations.generated.js +10 -0
  36. package/dist/store/sqlite/orders.d.ts +4 -0
  37. package/dist/store/sqlite/orders.js +6 -3
  38. package/dist/store/sqlite/schema.d.ts +64 -0
  39. package/dist/store/sqlite/schema.js +5 -0
  40. package/dist/store/sqlite/work-tasks.d.ts +7 -0
  41. package/dist/store/sqlite/work-tasks.js +10 -0
  42. 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
+ };
@@ -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,
@@ -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>;
@@ -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))),
@@ -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
+ };
@@ -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";
@@ -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({
@@ -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;
@@ -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 { acknowledgeMark } from "../marks.js";
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
+ });