@wirecat/cli-core 0.576.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 +20 -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/index.js +2 -1
- package/dist/mail/cli/marks.d.ts +6 -0
- package/dist/mail/cli/marks.js +69 -1
- package/dist/mail/findings.d.ts +2 -0
- package/dist/mail/findings.js +15 -1
- package/dist/mail/index.d.ts +1 -1
- package/dist/mail/index.js +1 -1
- package/dist/mail/marks.d.ts +29 -0
- package/dist/mail/marks.js +86 -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,26 @@ 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
|
+
|
|
10
30
|
## 0.576.0 — 05.11.2026
|
|
11
31
|
|
|
12
32
|
### 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"),
|
package/dist/mail/cli/index.js
CHANGED
|
@@ -19,7 +19,7 @@ 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
24
|
export { bulkTriageOf, MAIL_BULK_SETTINGS } from "./bulk-settings.js";
|
|
25
25
|
export { mailMark, markItem, markText } from "./marks.js";
|
|
@@ -254,6 +254,7 @@ const messagesCommand = (mail) => {
|
|
|
254
254
|
: itemOf(shown.find((_, at) => emails[at]?.anchor) ?? shown[0], unread ? { meta: { complete: false, issues: [unread] } } : undefined), () => shownText(emails));
|
|
255
255
|
});
|
|
256
256
|
messages.addCommand(acknowledgeCommand(mail));
|
|
257
|
+
messages.addCommand(resetMarkCommand(mail));
|
|
257
258
|
return messages;
|
|
258
259
|
};
|
|
259
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
|
+
});
|
package/dist/mail/findings.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export interface MailOrderFinding extends Cited {
|
|
|
99
99
|
invoice?: string;
|
|
100
100
|
payment?: string;
|
|
101
101
|
project?: string;
|
|
102
|
+
/** The order's page at the shop, http or https. */
|
|
103
|
+
sourceUrl?: string;
|
|
102
104
|
reason: string;
|
|
103
105
|
}
|
|
104
106
|
/** A later mail about a stored order, found by its shop and number: shipped, delivered, cancelled, returned. */
|
package/dist/mail/findings.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { EXTERNAL_ID_CHARS, externalIdOf, SOURCE_URL_CHARS, sourceUrlOf } from "../domain/external-links.js";
|
|
2
3
|
import { INVOICE_DIRECTIONS, MONEY_DIRECTIONS } from "../domain/finance.js";
|
|
3
4
|
import { MEMORY_KINDS, MEMORY_SCOPES } from "../domain/knowledge.js";
|
|
4
5
|
import { formatLocator, parseLocator } from "../domain/locator.js";
|
|
@@ -59,8 +60,19 @@ const optional = (key, value, read) => (value === undefined ? {} : { [key]: read
|
|
|
59
60
|
const one = (raw, ids) => {
|
|
60
61
|
if (!raw || typeof raw !== "object")
|
|
61
62
|
return invalid("a finding is an object");
|
|
62
|
-
if (raw.kind === "
|
|
63
|
+
if (raw.kind === "fact")
|
|
63
64
|
return validateFinding(raw, RULES, ids);
|
|
65
|
+
if (raw.kind === "task") {
|
|
66
|
+
const { externalId, sourceUrl, ...task } = raw;
|
|
67
|
+
return {
|
|
68
|
+
...validateFinding(task, RULES, ids),
|
|
69
|
+
...optional("externalId", externalId, (value) => {
|
|
70
|
+
const { provider, id } = externalIdOf(findingText(value, EXTERNAL_ID_CHARS + 40));
|
|
71
|
+
return `${provider}:${id}`;
|
|
72
|
+
}),
|
|
73
|
+
...optional("sourceUrl", sourceUrl, (value) => sourceUrlOf(findingText(value, SOURCE_URL_CHARS))),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
64
76
|
const id = findingId(raw.id, ids);
|
|
65
77
|
const evidence = validEvidence(raw.evidence, RULES);
|
|
66
78
|
const reference = (value, pattern, what) => {
|
|
@@ -247,6 +259,7 @@ const one = (raw, ids) => {
|
|
|
247
259
|
"invoice",
|
|
248
260
|
"payment",
|
|
249
261
|
"project",
|
|
262
|
+
"sourceUrl",
|
|
250
263
|
"reason",
|
|
251
264
|
"evidence",
|
|
252
265
|
], RULES.singular);
|
|
@@ -266,6 +279,7 @@ const one = (raw, ids) => {
|
|
|
266
279
|
...optional("invoice", raw.invoice, (value) => reference(value, INVOICE, "an order's invoice")),
|
|
267
280
|
...optional("payment", raw.payment, (value) => reference(value, PAYMENT, "an order's payment")),
|
|
268
281
|
...optional("project", raw.project, (value) => reference(value, PROJECT, "a project")),
|
|
282
|
+
...optional("sourceUrl", raw.sourceUrl, (value) => sourceUrlOf(findingText(value, SOURCE_URL_CHARS), "order.source-url")),
|
|
269
283
|
reason: findingText(raw.reason, 2000),
|
|
270
284
|
evidence,
|
|
271
285
|
};
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { dropMissing, type Gone, tooManyGone } from "./gone.js";
|
|
|
9
9
|
export { type AuthVerdicts, HEADER_FIELDS, headersOf, type MailHeaders, mailHeadersOf } from "./headers.js";
|
|
10
10
|
export { type MailIdsInput, mailIds } from "./ids.js";
|
|
11
11
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
12
|
-
export { acknowledgeMark, advancePastSkipped, type BulkExcerpt, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, type MailMark, type MailWaiting, type MarkRead, mailWaiting, markOf, marksOf, readPastMark, type SkipReason, } from "./marks.js";
|
|
12
|
+
export { acknowledgeMark, advancePastSkipped, type BulkExcerpt, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, type MailMark, type MailWaiting, type MarkRead, type MarkRewind, mailWaiting, markOf, marksOf, type RewoundMark, readPastMark, rewindMark, type SkipReason, } from "./marks.js";
|
|
13
13
|
export { BULK_TRIAGE_DEFAULTS, type BulkSorted, type BulkTriage, excerptOf, NEWSLETTER_DEFAULTS, NEWSLETTER_SENDERS, NEWSLETTER_WORDS, type NewsletterCues, newsletterSorter, ORDER_WORDS, } from "./newsletter.js";
|
|
14
14
|
export { type MailTurn, mailTurn } from "./owed.js";
|
|
15
15
|
export { type Address, htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, type ParsedMail, parseMail, } from "./parse.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -9,7 +9,7 @@ export { dropMissing, tooManyGone } from "./gone.js";
|
|
|
9
9
|
export { HEADER_FIELDS, headersOf, mailHeadersOf } from "./headers.js";
|
|
10
10
|
export { mailIds } from "./ids.js";
|
|
11
11
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
12
|
-
export { acknowledgeMark, advancePastSkipped, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, mailWaiting, markOf, marksOf, readPastMark, } from "./marks.js";
|
|
12
|
+
export { acknowledgeMark, advancePastSkipped, cursorOf, HOLD_PARTIAL_MS, HOLDER, MARK_BATCH, mailWaiting, markOf, marksOf, readPastMark, rewindMark, } from "./marks.js";
|
|
13
13
|
export { BULK_TRIAGE_DEFAULTS, excerptOf, NEWSLETTER_DEFAULTS, NEWSLETTER_SENDERS, NEWSLETTER_WORDS, newsletterSorter, ORDER_WORDS, } from "./newsletter.js";
|
|
14
14
|
export { mailTurn } from "./owed.js";
|
|
15
15
|
export { htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, parseMail, } from "./parse.js";
|
package/dist/mail/marks.d.ts
CHANGED
|
@@ -89,3 +89,32 @@ export declare const advancePastSkipped: (store: MessageStore, holder: string, n
|
|
|
89
89
|
* The same cursor twice changes nothing more.
|
|
90
90
|
*/
|
|
91
91
|
export declare const acknowledgeMark: (store: MessageStore, mailbox: AccountKey, holder: string, cursor: string, now?: number) => Promise<MailMark>;
|
|
92
|
+
/** Where the owner moves a holder's mark back to: the emails received since a time, or the last n it was given. */
|
|
93
|
+
export type MarkRewind = {
|
|
94
|
+
since: number;
|
|
95
|
+
} | {
|
|
96
|
+
last: number;
|
|
97
|
+
};
|
|
98
|
+
export interface RewoundMark {
|
|
99
|
+
mark: MailMark;
|
|
100
|
+
/** Where the mark was before. */
|
|
101
|
+
fromId: number;
|
|
102
|
+
/** The emails between the new mark and the old one the holder reads whole again. */
|
|
103
|
+
again: number;
|
|
104
|
+
/** Bulk emails between them: excerpts again, unless they are surely newsletters. */
|
|
105
|
+
bulk: number;
|
|
106
|
+
/** Emails between them the holder skips again: drafts, envelopes, suspicious senders, partial mail. */
|
|
107
|
+
skipped: number;
|
|
108
|
+
/** The mark is moved only when not dry. */
|
|
109
|
+
dry: boolean;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Moves the holder's mark back, so its next reviews give it that mail again: past every email received since
|
|
113
|
+
* `since`, or past the last `last` emails it was given. The owner's alone: a holder only ever moves its own mark
|
|
114
|
+
* forward. The id is the store's insert order, so an older email stored after the earliest one picked is read again too;
|
|
115
|
+
* the answer counts them all.
|
|
116
|
+
*/
|
|
117
|
+
export declare const rewindMark: (store: MessageStore, mailbox: AccountKey, holder: string, to: MarkRewind, { now, dry }?: {
|
|
118
|
+
now?: number;
|
|
119
|
+
dry?: boolean;
|
|
120
|
+
}) => Promise<RewoundMark>;
|
package/dist/mail/marks.js
CHANGED
|
@@ -316,3 +316,89 @@ export const acknowledgeMark = async (store, mailbox, holder, cursor, now = Date
|
|
|
316
316
|
await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
|
|
317
317
|
return { holder, ...next, updatedAt: now };
|
|
318
318
|
};
|
|
319
|
+
/**
|
|
320
|
+
* Moves the holder's mark back, so its next reviews give it that mail again: past every email received since
|
|
321
|
+
* `since`, or past the last `last` emails it was given. The owner's alone: a holder only ever moves its own mark
|
|
322
|
+
* forward. The id is the store's insert order, so an older email stored after the earliest one picked is read again too;
|
|
323
|
+
* the answer counts them all.
|
|
324
|
+
*/
|
|
325
|
+
export const rewindMark = async (store, mailbox, holder, to, { now = Date.now(), dry = false } = {}) => {
|
|
326
|
+
const key = keyOf(holder);
|
|
327
|
+
const account = await accountOf(store, mailbox);
|
|
328
|
+
if (!account)
|
|
329
|
+
throw new CliError("not_found", `no mail of ${mailbox.account} is stored`);
|
|
330
|
+
const mark = await markOf(store, mailbox, holder);
|
|
331
|
+
if (!mark)
|
|
332
|
+
throw new CliError("not_found", `${holder} has no mark in ${mailbox.account}: its first review starts it`, {
|
|
333
|
+
reason: "mail.no-mark",
|
|
334
|
+
});
|
|
335
|
+
if ("last" in to && (!Number.isSafeInteger(to.last) || to.last < 1))
|
|
336
|
+
throw new CliError("validation_error", "--last takes a whole number of emails, 1 or more");
|
|
337
|
+
if ("since" in to && !Number.isFinite(to.since))
|
|
338
|
+
throw new CliError("validation_error", "--since-time takes a time");
|
|
339
|
+
const given = (email) => {
|
|
340
|
+
const said = verdict(email, now);
|
|
341
|
+
return said === "read" || said === "held" || said === "bulk";
|
|
342
|
+
};
|
|
343
|
+
const between = [];
|
|
344
|
+
let through = mark.throughId;
|
|
345
|
+
let before = mark.throughId + 1;
|
|
346
|
+
for (let counted = 0;;) {
|
|
347
|
+
const page = await store.mail.emailsBefore(account.id, before, MARK_BATCH);
|
|
348
|
+
if (page.length === 0) {
|
|
349
|
+
if ("last" in to && between.length > 0)
|
|
350
|
+
through = 0;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
let done = false;
|
|
354
|
+
for (const email of page) {
|
|
355
|
+
if ("last" in to) {
|
|
356
|
+
between.push(email);
|
|
357
|
+
if (given(email))
|
|
358
|
+
counted += 1;
|
|
359
|
+
if (counted === to.last) {
|
|
360
|
+
through = email.id - 1;
|
|
361
|
+
done = true;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
// An email is stored after it arrives, so once stored before the time, none further back arrived after it.
|
|
367
|
+
if (email.storedAt < to.since - CLOCK_SLACK_MS) {
|
|
368
|
+
done = true;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
between.push(email);
|
|
372
|
+
if ((email.at ?? email.storedAt) >= to.since)
|
|
373
|
+
through = email.id - 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (done)
|
|
377
|
+
break;
|
|
378
|
+
before = page.at(-1)?.id ?? 0;
|
|
379
|
+
}
|
|
380
|
+
const moved = between.filter((email) => email.id > through);
|
|
381
|
+
const count = (pick) => moved.filter((email) => pick(verdict(email, now))).length;
|
|
382
|
+
const answer = {
|
|
383
|
+
fromId: mark.throughId,
|
|
384
|
+
again: count((said) => said === "read" || said === "held"),
|
|
385
|
+
bulk: count((said) => said === "bulk"),
|
|
386
|
+
skipped: count((said) => said !== "read" && said !== "held" && said !== "bulk"),
|
|
387
|
+
dry,
|
|
388
|
+
};
|
|
389
|
+
if (through >= mark.throughId)
|
|
390
|
+
return { mark, ...answer };
|
|
391
|
+
const [last] = through > 0 ? await store.mail.emailsBefore(account.id, through + 1, 1) : [];
|
|
392
|
+
const at = last?.id === through ? last.at : null;
|
|
393
|
+
const next = {
|
|
394
|
+
throughId: through,
|
|
395
|
+
throughAt: at,
|
|
396
|
+
held: mark.held.filter((id) => id <= through),
|
|
397
|
+
suspicious: mark.suspicious.filter((id) => id <= through),
|
|
398
|
+
read: mark.read,
|
|
399
|
+
skipped: mark.skipped,
|
|
400
|
+
};
|
|
401
|
+
if (!dry)
|
|
402
|
+
await store.sync.setSyncState(mailbox, key, JSON.stringify(next));
|
|
403
|
+
return { mark: { holder, ...next, updatedAt: dry ? mark.updatedAt : now }, ...answer };
|
|
404
|
+
};
|
|
@@ -39,6 +39,10 @@ export interface TaskFinding extends FindingBase {
|
|
|
39
39
|
dueEvidence?: string[];
|
|
40
40
|
assignee?: string;
|
|
41
41
|
assigneeEvidence?: string[];
|
|
42
|
+
/** Mail only: `<service>:<id>` in the outside service the mail came from, such as `avito:4512345678`. */
|
|
43
|
+
externalId?: string;
|
|
44
|
+
/** Mail only: the http or https page the task is about, such as the conversation the mail links to. */
|
|
45
|
+
sourceUrl?: string;
|
|
42
46
|
}
|
|
43
47
|
export type Finding = FactFinding | TaskFinding | PaymentFinding;
|
|
44
48
|
export interface FindingsInput {
|
|
@@ -67,6 +67,8 @@ export interface MailActivityStore {
|
|
|
67
67
|
* processing mark reads next. The id is the store's insert order, so mail stored late is never behind the mark.
|
|
68
68
|
*/
|
|
69
69
|
emailsAfter(accountId: number, afterId: number, limit: number, ids?: readonly number[]): Promise<MarkedEmail[]>;
|
|
70
|
+
/** The account's live emails stored before `beforeId`, newest first, at most `limit`: what a mark moved back reads. */
|
|
71
|
+
emailsBefore(accountId: number, beforeId: number, limit: number): Promise<MarkedEmail[]>;
|
|
70
72
|
/** The account's newest store id; 0 with no mail. */
|
|
71
73
|
newestEmailId(accountId: number): Promise<number>;
|
|
72
74
|
/** Every `sync_cursors` value of the account whose key starts with `prefix`. */
|
|
@@ -125,6 +125,10 @@ export const emailsAfter = (database, accountId, afterId, limit, ids = []) => {
|
|
|
125
125
|
};
|
|
126
126
|
export const mailActivity = (database) => ({
|
|
127
127
|
emailsAfter: async (accountId, afterId, limit, ids) => emailsAfter(database, accountId, afterId, limit, ids),
|
|
128
|
+
emailsBefore: async (accountId, beforeId, limit) => database
|
|
129
|
+
.prepare(`${MARKED} WHERE e.id < ? AND e.account_id = ? AND e.deleted_at IS NULL ORDER BY e.id DESC LIMIT ?`)
|
|
130
|
+
.all(beforeId, accountId, limit)
|
|
131
|
+
.map(markedOf),
|
|
128
132
|
newestEmailId: async (accountId) => Number(database.prepare("SELECT max(id) AS id FROM emails WHERE account_id = ?").get(accountId)?.id ?? 0),
|
|
129
133
|
statesLike: async (accountId, prefix) => database
|
|
130
134
|
.prepare("SELECT key, value, updated_at FROM sync_cursors WHERE account_id = ? AND substr(key, 1, ?) = ?")
|
|
@@ -93,6 +93,8 @@ export const findingsCoreOver = (context, kind) => {
|
|
|
93
93
|
type: finding.type,
|
|
94
94
|
description: `${finding.commitment}: ${finding.reason}`,
|
|
95
95
|
...(finding.dueAt ? { dueAt: new Date(finding.dueAt) } : {}),
|
|
96
|
+
...(finding.externalId ? { externalId: finding.externalId } : {}),
|
|
97
|
+
...(finding.sourceUrl ? { sourceUrl: finding.sourceUrl } : {}),
|
|
96
98
|
}, agent);
|
|
97
99
|
const id = taskRowOf(database, task.id);
|
|
98
100
|
if (id === undefined)
|
|
@@ -67,6 +67,7 @@ export const MANIFEST = [
|
|
|
67
67
|
{ name: "20261104232153_version-49-relation-amounts", version: 49, minCompatible: 34 },
|
|
68
68
|
{ name: "20261105125314_version-50-orders", version: 50, minCompatible: 34 },
|
|
69
69
|
{ name: "20261105125315_version-50-orders-rules", version: 50, minCompatible: 34 },
|
|
70
|
+
{ name: "20261105144549_version-51-task-links", version: 51, minCompatible: 34 },
|
|
70
71
|
];
|
|
71
72
|
export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
|
|
72
73
|
const migrations = [];
|
|
@@ -974,5 +974,15 @@ export const GENERATED = [
|
|
|
974
974
|
"-- The store's code checks these first and says why; the triggers keep a stray write from another build or a hand\n-- edit from storing an order with an unknown status or purpose, or an amount without its currency.\nCREATE TRIGGER orders_valid_bi BEFORE INSERT ON orders\n WHEN new.customer_type NOT IN ('person', 'organization')\n OR new.purpose NOT IN ('personal', 'business', 'client', 'other')\n OR new.status NOT IN ('placed', 'shipped', 'delivered', 'cancelled', 'returned')\n OR new.review NOT IN ('proposed', 'confirmed', 'rejected')\n OR (new.total IS NULL) <> (new.currency IS NULL) OR new.total < 0 BEGIN\n SELECT RAISE(ABORT, 'orders: a person or organization customer, a known purpose, status and review, an amount with its currency');\nEND;",
|
|
975
975
|
"CREATE TRIGGER orders_valid_bu BEFORE UPDATE ON orders\n WHEN new.customer_type NOT IN ('person', 'organization')\n OR new.purpose NOT IN ('personal', 'business', 'client', 'other')\n OR new.status NOT IN ('placed', 'shipped', 'delivered', 'cancelled', 'returned')\n OR new.review NOT IN ('proposed', 'confirmed', 'rejected')\n OR (new.total IS NULL) <> (new.currency IS NULL) OR new.total < 0 BEGIN\n SELECT RAISE(ABORT, 'orders: a person or organization customer, a known purpose, status and review, an amount with its currency');\nEND;"
|
|
976
976
|
]
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
"name": "20261105144549_version-51-task-links",
|
|
980
|
+
"statements": [
|
|
981
|
+
"ALTER TABLE `orders` ADD `source_url` text;",
|
|
982
|
+
"ALTER TABLE `tasks` ADD `provider` text;",
|
|
983
|
+
"ALTER TABLE `tasks` ADD `external_id` text;",
|
|
984
|
+
"ALTER TABLE `tasks` ADD `source_url` text;",
|
|
985
|
+
"CREATE INDEX `tasks_by_provider_external_id` ON `tasks` (`provider`,`external_id`) WHERE external_id IS NOT NULL;"
|
|
986
|
+
]
|
|
977
987
|
}
|
|
978
988
|
];
|
|
@@ -26,6 +26,8 @@ export interface Order extends Reviewed {
|
|
|
26
26
|
fulfilledDate: string | null;
|
|
27
27
|
invoice: string | null;
|
|
28
28
|
payment: string | null;
|
|
29
|
+
/** The order's page at the shop, http or https. */
|
|
30
|
+
sourceUrl: string | null;
|
|
29
31
|
}
|
|
30
32
|
export interface OrderInput extends Written {
|
|
31
33
|
/** `person:` or `organization:`; the owner's own person when left out. */
|
|
@@ -43,6 +45,8 @@ export interface OrderInput extends Written {
|
|
|
43
45
|
fulfilledDate?: string;
|
|
44
46
|
invoice?: string;
|
|
45
47
|
payment?: string;
|
|
48
|
+
/** The order's page at the shop: an http or https address. */
|
|
49
|
+
sourceUrl?: string;
|
|
46
50
|
}
|
|
47
51
|
export interface OrderFilter {
|
|
48
52
|
status?: OrderStatus;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { sourceUrlOf } from "../../domain/external-links.js";
|
|
2
3
|
import { FINANCE_REVIEWS } from "../../domain/finance.js";
|
|
3
4
|
import { currencyOf, formatDecimal, minorUnits, scaleOf } from "../../domain/money.js";
|
|
4
5
|
import { ORDER_PURPOSES, ORDER_STATUSES, orderTitle } from "../../domain/orders.js";
|
|
@@ -42,6 +43,7 @@ export const orderOperationsOver = (context) => {
|
|
|
42
43
|
fulfilledDate: row.fulfilled_date == null ? null : String(row.fulfilled_date),
|
|
43
44
|
invoice: row.invoice_id == null ? null : `invoice:${row.invoice_id}`,
|
|
44
45
|
payment: row.payment_id == null ? null : `payment:${row.payment_id}`,
|
|
46
|
+
sourceUrl: row.source_url == null ? null : String(row.source_url),
|
|
45
47
|
...reviewedOf(row, "order"),
|
|
46
48
|
};
|
|
47
49
|
};
|
|
@@ -96,6 +98,7 @@ export const orderOperationsOver = (context) => {
|
|
|
96
98
|
refuse("an order's amount is 0 or more", "money.amount");
|
|
97
99
|
const orderDate = isoDate(input.orderDate, "an order's date");
|
|
98
100
|
const fulfilledDate = isoDate(input.fulfilledDate, "an order's delivery date");
|
|
101
|
+
const sourceUrl = input.sourceUrl === undefined ? null : sourceUrlOf(input.sourceUrl, "order.source-url");
|
|
99
102
|
const id = write(database, () => {
|
|
100
103
|
if (number !== null && ordersOfShop(database, supplierName, number).length)
|
|
101
104
|
refuse(`order ${number} from ${supplierName} is already stored; change its status instead`, "order.duplicate");
|
|
@@ -108,9 +111,9 @@ export const orderOperationsOver = (context) => {
|
|
|
108
111
|
const made = Number(database
|
|
109
112
|
.prepare("INSERT INTO orders (customer_type, customer_id, purpose, supplier_type, supplier_id, supplier_name, number, " +
|
|
110
113
|
"currency, total, status, order_date, fulfilled_date, invoice_id, payment_id, project_id, review, " +
|
|
111
|
-
"author_type, author_id, model, confirmed_by_type, confirmed_by_id, source, metadata, created_at, updated_at
|
|
112
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
113
|
-
.get(customer.type, customer.id, purpose, supplier?.type ?? null, supplier?.id ?? null, supplierName, number, currency, total, status, orderDate, fulfilledDate, invoice, payment, by.project, by.review, by.actor.type, by.actor.id, by.model, by.confirmedType, by.confirmedId, by.source, by.metadata, at, at)?.id);
|
|
114
|
+
"author_type, author_id, model, confirmed_by_type, confirmed_by_id, source, metadata, created_at, updated_at, " +
|
|
115
|
+
"source_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")
|
|
116
|
+
.get(customer.type, customer.id, purpose, supplier?.type ?? null, supplier?.id ?? null, supplierName, number, currency, total, status, orderDate, fulfilledDate, invoice, payment, by.project, by.review, by.actor.type, by.actor.id, by.model, by.confirmedType, by.confirmedId, by.source, by.metadata, at, at, sourceUrl)?.id);
|
|
114
117
|
keepEvidence("order", made, by);
|
|
115
118
|
return made;
|
|
116
119
|
});
|
|
@@ -12747,6 +12747,54 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
12747
12747
|
identity: undefined;
|
|
12748
12748
|
generated: undefined;
|
|
12749
12749
|
}, {}>;
|
|
12750
|
+
provider: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12751
|
+
name: string;
|
|
12752
|
+
tableName: "tasks";
|
|
12753
|
+
dataType: "string";
|
|
12754
|
+
data: string;
|
|
12755
|
+
driverParam: string;
|
|
12756
|
+
notNull: false;
|
|
12757
|
+
hasDefault: false;
|
|
12758
|
+
isPrimaryKey: false;
|
|
12759
|
+
isAutoincrement: false;
|
|
12760
|
+
hasRuntimeDefault: false;
|
|
12761
|
+
enumValues: [string, ...string[]];
|
|
12762
|
+
baseColumn: never;
|
|
12763
|
+
identity: undefined;
|
|
12764
|
+
generated: undefined;
|
|
12765
|
+
}, {}>;
|
|
12766
|
+
externalId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12767
|
+
name: string;
|
|
12768
|
+
tableName: "tasks";
|
|
12769
|
+
dataType: "string";
|
|
12770
|
+
data: string;
|
|
12771
|
+
driverParam: string;
|
|
12772
|
+
notNull: false;
|
|
12773
|
+
hasDefault: false;
|
|
12774
|
+
isPrimaryKey: false;
|
|
12775
|
+
isAutoincrement: false;
|
|
12776
|
+
hasRuntimeDefault: false;
|
|
12777
|
+
enumValues: [string, ...string[]];
|
|
12778
|
+
baseColumn: never;
|
|
12779
|
+
identity: undefined;
|
|
12780
|
+
generated: undefined;
|
|
12781
|
+
}, {}>;
|
|
12782
|
+
sourceUrl: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
12783
|
+
name: string;
|
|
12784
|
+
tableName: "tasks";
|
|
12785
|
+
dataType: "string";
|
|
12786
|
+
data: string;
|
|
12787
|
+
driverParam: string;
|
|
12788
|
+
notNull: false;
|
|
12789
|
+
hasDefault: false;
|
|
12790
|
+
isPrimaryKey: false;
|
|
12791
|
+
isAutoincrement: false;
|
|
12792
|
+
hasRuntimeDefault: false;
|
|
12793
|
+
enumValues: [string, ...string[]];
|
|
12794
|
+
baseColumn: never;
|
|
12795
|
+
identity: undefined;
|
|
12796
|
+
generated: undefined;
|
|
12797
|
+
}, {}>;
|
|
12750
12798
|
};
|
|
12751
12799
|
dialect: 'sqlite';
|
|
12752
12800
|
isAlias: false;
|
|
@@ -15326,6 +15374,22 @@ export declare const orders: import("drizzle-orm/sqlite-core").SQLiteTableWithCo
|
|
|
15326
15374
|
identity: undefined;
|
|
15327
15375
|
generated: undefined;
|
|
15328
15376
|
}, {}>;
|
|
15377
|
+
sourceUrl: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
15378
|
+
name: string;
|
|
15379
|
+
tableName: "orders";
|
|
15380
|
+
dataType: "string";
|
|
15381
|
+
data: string;
|
|
15382
|
+
driverParam: string;
|
|
15383
|
+
notNull: false;
|
|
15384
|
+
hasDefault: false;
|
|
15385
|
+
isPrimaryKey: false;
|
|
15386
|
+
isAutoincrement: false;
|
|
15387
|
+
hasRuntimeDefault: false;
|
|
15388
|
+
enumValues: [string, ...string[]];
|
|
15389
|
+
baseColumn: never;
|
|
15390
|
+
identity: undefined;
|
|
15391
|
+
generated: undefined;
|
|
15392
|
+
}, {}>;
|
|
15329
15393
|
};
|
|
15330
15394
|
dialect: 'sqlite';
|
|
15331
15395
|
isAlias: false;
|
|
@@ -1271,6 +1271,9 @@ export const tasks = sqliteTable("tasks", {
|
|
|
1271
1271
|
priorityScore: real("priority_score"),
|
|
1272
1272
|
agent: text("agent"),
|
|
1273
1273
|
revision: integer("revision").notNull().default(1),
|
|
1274
|
+
provider: text("provider"),
|
|
1275
|
+
externalId: text("external_id"),
|
|
1276
|
+
sourceUrl: text("source_url"),
|
|
1274
1277
|
}, (table) => [
|
|
1275
1278
|
unique().on(table.projectId, table.number),
|
|
1276
1279
|
index("tasks_by_title_folded").on(table.titleFolded),
|
|
@@ -1283,6 +1286,7 @@ export const tasks = sqliteTable("tasks", {
|
|
|
1283
1286
|
index("tasks_by_project_id_status_next_check_at").on(table.projectId, table.status, table.nextCheckAt),
|
|
1284
1287
|
index("tasks_by_project_id_source_locator").on(table.projectId, table.sourceLocator),
|
|
1285
1288
|
index("tasks_by_source_group").on(table.sourceGroup),
|
|
1289
|
+
index("tasks_by_provider_external_id").on(table.provider, table.externalId).where(sql `external_id IS NOT NULL`),
|
|
1286
1290
|
index("tasks_by_parent_id").on(table.parentId),
|
|
1287
1291
|
index("tasks_by_closed_by_type_closed_by_id").on(table.closedByType, table.closedById),
|
|
1288
1292
|
index("tasks_by_author_type_author_id").on(table.authorType, table.authorId),
|
|
@@ -1521,6 +1525,7 @@ export const orders = sqliteTable("orders", {
|
|
|
1521
1525
|
createdAt: integer("created_at").notNull(),
|
|
1522
1526
|
updatedAt: integer("updated_at").notNull(),
|
|
1523
1527
|
deletedAt: integer("deleted_at"),
|
|
1528
|
+
sourceUrl: text("source_url"),
|
|
1524
1529
|
}, (table) => [
|
|
1525
1530
|
index("orders_by_customer_type_customer_id").on(table.customerType, table.customerId),
|
|
1526
1531
|
index("orders_by_supplier_type_supplier_id").on(table.supplierType, table.supplierId),
|
|
@@ -39,6 +39,10 @@ export interface TaskFields {
|
|
|
39
39
|
project?: string;
|
|
40
40
|
/** A project task's kind, bug, feature or chore. */
|
|
41
41
|
type?: WorkTaskType;
|
|
42
|
+
/** `<service>:<id>`, the task's id in an outside service, such as `avito:4512345678`; `null` clears it. */
|
|
43
|
+
externalId?: string | null;
|
|
44
|
+
/** The http or https page the task is about, such as an Avito ad; `null` clears it. */
|
|
45
|
+
sourceUrl?: string | null;
|
|
42
46
|
}
|
|
43
47
|
export interface NewWorkTask extends Omit<TaskFields, "reason" | "project" | "type"> {
|
|
44
48
|
/** `project:<id>` or the project's key. */
|
|
@@ -75,6 +79,9 @@ export interface TaskDetails {
|
|
|
75
79
|
parent: string | null;
|
|
76
80
|
/** What a task found in a message points at; null for a task made here. */
|
|
77
81
|
source: string | null;
|
|
82
|
+
/** `<service>:<id>` in an outside service, such as `avito:4512345678`. */
|
|
83
|
+
externalId: string | null;
|
|
84
|
+
sourceUrl: string | null;
|
|
78
85
|
dueAt: string | null;
|
|
79
86
|
nextCheckAt: string | null;
|
|
80
87
|
closedAt: string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
3
|
+
import { externalIdOf, externalIdText, sourceUrlOf } from "../../domain/external-links.js";
|
|
3
4
|
import { TASK_TYPES, WORK_TASK_TYPES } from "../../domain/knowledge.js";
|
|
4
5
|
import { checkedScore, isClosedState, legacyPriority, PRIORITY_LABELS, priorityBand, priorityLabel, TASK_STATES, } from "../../tasks/index.js";
|
|
5
6
|
import { folded } from "./folded.js";
|
|
@@ -136,6 +137,8 @@ export const workTaskOperationsOver = (context) => {
|
|
|
136
137
|
revision: Number(row.revision),
|
|
137
138
|
parent: row.parent_id == null ? null : reference(Number(row.parent_id)),
|
|
138
139
|
source: row.source_locator == null ? null : String(row.source_locator),
|
|
140
|
+
externalId: externalIdText(row.provider, row.external_id),
|
|
141
|
+
sourceUrl: row.source_url == null ? null : String(row.source_url),
|
|
139
142
|
dueAt: iso(row.due_at),
|
|
140
143
|
nextCheckAt: iso(row.next_check_at),
|
|
141
144
|
closedAt: iso(row.closed_at),
|
|
@@ -282,6 +285,13 @@ export const workTaskOperationsOver = (context) => {
|
|
|
282
285
|
change("close_reason", reason);
|
|
283
286
|
change("close_reason_folded", folded(reason));
|
|
284
287
|
}
|
|
288
|
+
if (fields.externalId !== undefined) {
|
|
289
|
+
const external = fields.externalId === null ? null : externalIdOf(fields.externalId);
|
|
290
|
+
change("provider", external?.provider ?? null);
|
|
291
|
+
change("external_id", external?.id ?? null);
|
|
292
|
+
}
|
|
293
|
+
if (fields.sourceUrl !== undefined)
|
|
294
|
+
change("source_url", fields.sourceUrl === null ? null : sourceUrlOf(fields.sourceUrl));
|
|
285
295
|
if (fields.category !== undefined)
|
|
286
296
|
change("category", fields.category, "category");
|
|
287
297
|
if (fields.type !== undefined && fields.type !== before.type) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.577.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|