@wirecat/cli-core 0.471.0 → 0.473.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 (76) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cli/execution.d.ts +5 -0
  3. package/dist/cli/execution.js +5 -0
  4. package/dist/cli/help/first-screen.js +5 -0
  5. package/dist/cli/messenger/conversation-answers.d.ts +1 -0
  6. package/dist/cli/messenger/hearing-command.js +1 -1
  7. package/dist/cli/messenger/message-answers.d.ts +1 -0
  8. package/dist/cli/messenger/message-items.js +2 -0
  9. package/dist/cli/messenger/message-writes.d.ts +2 -0
  10. package/dist/cli/program.js +5 -3
  11. package/dist/entities/conversation-results.d.ts +2 -0
  12. package/dist/entities/evidence.d.ts +8 -0
  13. package/dist/entities/index.d.ts +19 -0
  14. package/dist/entities/knowledge-lists.d.ts +1 -0
  15. package/dist/entities/knowledge-lists.js +1 -0
  16. package/dist/entities/listener.d.ts +4 -0
  17. package/dist/entities/message-context.d.ts +3 -0
  18. package/dist/entities/message-write.d.ts +4 -0
  19. package/dist/entities/message.d.ts +1 -0
  20. package/dist/entities/message.js +1 -0
  21. package/dist/entities/store-records.d.ts +1 -0
  22. package/dist/errors/reasons.js +1 -0
  23. package/dist/mail/bulk.d.ts +7 -0
  24. package/dist/mail/bulk.js +3 -0
  25. package/dist/mail/facts.d.ts +9 -0
  26. package/dist/mail/facts.js +22 -0
  27. package/dist/mail/folders.d.ts +10 -0
  28. package/dist/mail/folders.js +13 -0
  29. package/dist/mail/gone.d.ts +20 -0
  30. package/dist/mail/gone.js +53 -0
  31. package/dist/mail/headers.d.ts +43 -0
  32. package/dist/mail/headers.js +88 -0
  33. package/dist/mail/index.d.ts +17 -0
  34. package/dist/mail/index.js +16 -0
  35. package/dist/mail/parse.d.ts +24 -0
  36. package/dist/mail/parse.js +79 -0
  37. package/dist/mail/presets.d.ts +30 -0
  38. package/dist/mail/presets.js +95 -0
  39. package/dist/mail/probe.d.ts +49 -0
  40. package/dist/mail/probe.js +81 -0
  41. package/dist/mail/resume.d.ts +61 -0
  42. package/dist/mail/resume.js +61 -0
  43. package/dist/mail/rule-input.d.ts +14 -0
  44. package/dist/mail/rule-input.js +32 -0
  45. package/dist/mail/rule-tags.d.ts +40 -0
  46. package/dist/mail/rule-tags.js +87 -0
  47. package/dist/mail/rules.d.ts +83 -0
  48. package/dist/mail/rules.js +108 -0
  49. package/dist/mail/score.d.ts +67 -0
  50. package/dist/mail/score.js +174 -0
  51. package/dist/mail/sender-scores.d.ts +30 -0
  52. package/dist/mail/sender-scores.js +96 -0
  53. package/dist/mail/source.d.ts +74 -0
  54. package/dist/mail/source.js +1 -0
  55. package/dist/mail/stored.d.ts +7 -0
  56. package/dist/mail/stored.js +36 -0
  57. package/dist/mail/sync.d.ts +106 -0
  58. package/dist/mail/sync.js +748 -0
  59. package/dist/mail/testing/contract.d.ts +20 -0
  60. package/dist/mail/testing/contract.js +106 -0
  61. package/dist/mail/testing/fake.d.ts +72 -0
  62. package/dist/mail/testing/fake.js +156 -0
  63. package/dist/mail/testing/index.d.ts +2 -0
  64. package/dist/mail/testing/index.js +2 -0
  65. package/dist/runs/error-detail.d.ts +2 -0
  66. package/dist/runs/error-detail.js +4 -0
  67. package/dist/speech/hearing.d.ts +2 -0
  68. package/dist/speech/hearing.js +10 -0
  69. package/dist/store/knowledge-query.js +3 -1
  70. package/dist/store/people.d.ts +2 -0
  71. package/dist/store/people.js +12 -0
  72. package/dist/store/sqlite/knowledge.d.ts +3 -1
  73. package/dist/store/sqlite/knowledge.js +7 -2
  74. package/dist/store/sqlite/task-keys.d.ts +1 -0
  75. package/dist/store/sqlite/task-keys.js +1 -0
  76. package/package.json +12 -1
@@ -0,0 +1,20 @@
1
+ import type { ContractCase } from "../../meetings/index.js";
2
+ import type { MessageStore } from "../../store/index.js";
3
+ import type { MailSource } from "../source.js";
4
+ import type { FakeMail } from "./fake.js";
5
+ /** The fixtures every adapter is seeded with: its harness makes a mailbox that holds exactly these. */
6
+ export declare const CONTRACT_MAIL: FakeMail[];
7
+ /** An adapter over a mailbox holding `CONTRACT_MAIL`, fresh for each case. */
8
+ export type MailSourceFixture = () => MailSource | Promise<MailSource>;
9
+ /** What every adapter must answer; the optional members are checked only where the source has them. */
10
+ export declare const mailSourceContract: (make: MailSourceFixture) => ContractCase[];
11
+ /** A source over `CONTRACT_MAIL` and the store it syncs into, fresh for each case. */
12
+ export type MailSyncFixture = () => {
13
+ source: MailSource;
14
+ store: MessageStore;
15
+ } | Promise<{
16
+ source: MailSource;
17
+ store: MessageStore;
18
+ }>;
19
+ /** What `syncMail` must do with any adapter: store each message once under the address's account, and only once. */
20
+ export declare const mailSyncContract: (make: MailSyncFixture) => ContractCase[];
@@ -0,0 +1,106 @@
1
+ import assert from "node:assert/strict";
2
+ import { GMAIL_ALL_MAIL } from "../folders.js";
3
+ import { parseMail } from "../parse.js";
4
+ import { syncMail } from "../sync.js";
5
+ const SINCE = new Date("2026-09-01T00:00:00Z");
6
+ /** The fixtures every adapter is seeded with: its harness makes a mailbox that holds exactly these. */
7
+ export const CONTRACT_MAIL = [1, 2, 3].map((n) => ({
8
+ uid: String(100 + n),
9
+ messageId: String(9000 + n),
10
+ threadId: String(n === 3 ? 9001 : 9000 + n),
11
+ receivedAt: `2026-09-${String(10 + n).padStart(2, "0")}T08:00:00Z`,
12
+ subject: `Contract subject ${n}`,
13
+ from: ["Rin Example", "rin@example.test"],
14
+ to: [[null, "owner@example.test"]],
15
+ text: `Contract body ${n}`,
16
+ }));
17
+ /** What every adapter must answer; the optional members are checked only where the source has them. */
18
+ export const mailSourceContract = (make) => {
19
+ const test = (name, check) => ({
20
+ name,
21
+ run: async () => check(await make()),
22
+ });
23
+ const folder = GMAIL_ALL_MAIL;
24
+ return [
25
+ test("lists each message once with a stable id, a thread id and a time inside the window", async (source) => {
26
+ const listed = await source.list(SINCE, folder);
27
+ assert.equal(listed.length, CONTRACT_MAIL.length);
28
+ assert.equal(new Set(listed.map(({ messageId }) => messageId)).size, listed.length);
29
+ for (const item of listed) {
30
+ assert.ok(item.uid && item.messageId && item.threadId, "ids");
31
+ assert.ok(Date.parse(item.receivedAt) >= SINCE.getTime(), "inside the window");
32
+ }
33
+ }),
34
+ test("lists a stable id for the same message on every listing", async (source) => {
35
+ const ids = async () => (await source.list(SINCE, folder)).map(({ messageId }) => messageId).sort();
36
+ assert.deepEqual(await ids(), await ids());
37
+ }),
38
+ test("reads the sender and recipients with `headers`", async (source) => {
39
+ const listed = await source.list(SINCE, folder, { headers: true });
40
+ for (const item of listed) {
41
+ assert.equal(item.headers?.from, "rin@example.test");
42
+ assert.deepEqual(item.headers?.to, ["owner@example.test"]);
43
+ }
44
+ }),
45
+ test("lists only a search's UIDs, and names the UIDVALIDITY", async (source) => {
46
+ if (source.search === undefined)
47
+ return;
48
+ const searched = await source.search(SINCE, folder);
49
+ assert.equal(searched.uids.length, CONTRACT_MAIL.length);
50
+ const [first] = searched.uids;
51
+ const listed = await source.list(SINCE, folder, {
52
+ searched: { ...searched, uids: first === undefined ? [] : [first] },
53
+ });
54
+ assert.deepEqual(listed.map(({ uid }) => uid), first === undefined ? [] : [first]);
55
+ }),
56
+ test("returns each message's bytes in order, and one that cannot be read as an Error, not a failed call", async (source) => {
57
+ const listed = await source.list(SINCE, folder);
58
+ const raws = await source.fetchRaw(listed);
59
+ assert.equal(raws.length, listed.length);
60
+ for (const [index, raw] of raws.entries()) {
61
+ assert.ok(raw instanceof Uint8Array, "bytes");
62
+ const parsed = await parseMail(raw, 10_000);
63
+ assert.match(parsed.subject ?? "", /^Contract subject \d$/);
64
+ assert.equal(parsed.subject, CONTRACT_MAIL.find(({ messageId }) => messageId === listed[index]?.messageId)?.subject);
65
+ }
66
+ const [first] = listed;
67
+ if (first !== undefined) {
68
+ const [unknown] = await source.fetchRaw([{ ...first, uid: "999999" }]);
69
+ assert.ok(unknown instanceof Error);
70
+ }
71
+ }),
72
+ test("finds messages by id, leaving out one that is gone", async (source) => {
73
+ if (source.find === undefined)
74
+ return;
75
+ const found = await source.find(["9001", "no-such-id"], folder);
76
+ assert.deepEqual(found.map(({ messageId }) => messageId), ["9001"]);
77
+ }),
78
+ test("says which UIDs a folder still holds", async (source) => {
79
+ if (source.present === undefined)
80
+ return;
81
+ assert.deepEqual(await source.present(["101", "999999"], folder), ["101"]);
82
+ }),
83
+ ];
84
+ };
85
+ /** What `syncMail` must do with any adapter: store each message once under the address's account, and only once. */
86
+ export const mailSyncContract = (make) => {
87
+ const address = "owner@example.test";
88
+ const account = { address };
89
+ return [
90
+ {
91
+ name: "stores each message as an email of its thread under the address's account, and a rerun adds nothing",
92
+ run: async () => {
93
+ const { source, store } = await make();
94
+ const first = await syncMail(source, store, { account, since: SINCE });
95
+ assert.equal(first.saved, CONTRACT_MAIL.length);
96
+ assert.equal(first.complete, true);
97
+ const stored = await store.accounts.storedAccount({ provider: "email", account: address });
98
+ const emails = await store.mail.emails({ accountId: stored.id });
99
+ assert.deepEqual(emails.map(({ externalId }) => externalId).sort(), CONTRACT_MAIL.map(({ messageId }) => messageId));
100
+ const again = await syncMail(source, store, { account, since: SINCE });
101
+ assert.equal(again.saved, 0);
102
+ assert.equal(again.deleted, 0);
103
+ },
104
+ },
105
+ ];
106
+ };
@@ -0,0 +1,72 @@
1
+ import type { FileStamp } from "../resume.js";
2
+ import type { MailSource } from "../source.js";
3
+ export type FakePerson = [name: string | null, address: string];
4
+ /** One message of a fake Gmail-like mailbox: its ids, envelope and the parts `rawOf` writes as RFC 5322. */
5
+ export interface FakeMail {
6
+ uid: string;
7
+ messageId: string;
8
+ threadId: string;
9
+ receivedAt: string;
10
+ subject: string;
11
+ from: FakePerson;
12
+ to: FakePerson[];
13
+ cc?: FakePerson[];
14
+ text?: string;
15
+ html?: string;
16
+ listId?: string;
17
+ attachment?: {
18
+ name: string;
19
+ mime: string;
20
+ content: string;
21
+ };
22
+ extraHeaders?: string[];
23
+ /** Plain label names, as a listing gives them; `flagged` adds Starred. */
24
+ labels?: string[];
25
+ seen?: boolean;
26
+ flagged?: boolean;
27
+ /** `trash` puts it in the trash folder; otherwise it is in All Mail, which INBOX also shows. */
28
+ folder?: "trash";
29
+ }
30
+ /** The mailbox a fake source reads live, so a test changes it between syncs; the switches make the source fail. */
31
+ export interface FakeMailbox {
32
+ messages: FakeMail[];
33
+ /** All Mail under the account's language, as Gmail names it. */
34
+ allMail?: string;
35
+ trash?: string;
36
+ validity?: number;
37
+ /** Each answer of `search` and `list` ends in this failure. */
38
+ searchFails?: string;
39
+ /** A refused header fetch fails the whole listing. */
40
+ headersFail?: boolean;
41
+ moveFails?: boolean;
42
+ /** UIDs whose raw bytes cannot be read: `fetchRaw` answers an Error for each of them, and only for them. */
43
+ unreadable?: string[];
44
+ /** UIDs that fail once, as a server's backend error does: `list` leaves each out, `fetchRaw` answers an Error. */
45
+ failOnce?: string[];
46
+ /** Calls the source answered, by kind, so a test counts a budget's requests. */
47
+ calls?: {
48
+ search: number;
49
+ list: number;
50
+ fetchRaw: number;
51
+ };
52
+ /** What the source tells a budgeted sync about its batch sizes. */
53
+ listBatch?: number;
54
+ fetchBatch?: number;
55
+ /** What the source was asked: the UIDs of each listing, and `msgid:<id>` for each `find`. */
56
+ asked?: string[];
57
+ }
58
+ /** The RFC 5322 bytes of a fake message, CRLF line ends, the body and attachment in base64. */
59
+ export declare const rawOf: (m: FakeMail) => Uint8Array;
60
+ /**
61
+ * An in-memory mail source with Gmail's shape: ids and threads that outlive a folder, labels and flags, UIDVALIDITY,
62
+ * a trash a move reaches, and All Mail under the account's language. The sync tests and the adapter contract run on it.
63
+ */
64
+ export declare const fakeMailSource: (mailbox: FakeMailbox) => MailSource;
65
+ /**
66
+ * A snapshot source, as an mbox file is: every message since a date with no UIDs, one stamp that changes when the
67
+ * file does, and no search, find or folders. `archive.mails` and `archive.stamp` are read live.
68
+ */
69
+ export declare const fakeArchiveSource: (archive: {
70
+ mails: FakeMail[];
71
+ stamp: FileStamp;
72
+ }) => MailSource;
@@ -0,0 +1,156 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { GMAIL_ALL_MAIL } from "../folders.js";
3
+ import { headersOf } from "../headers.js";
4
+ const TRASH = "[Gmail]/Bin";
5
+ const person = ([name, address]) => (name ? `"${name}" <${address}>` : address);
6
+ /** The RFC 5322 bytes of a fake message, CRLF line ends, the body and attachment in base64. */
7
+ export const rawOf = (m) => {
8
+ const type = m.html ? "text/html" : "text/plain";
9
+ const headers = [
10
+ `From: ${person(m.from)}`,
11
+ `To: ${m.to.map(person).join(", ")}`,
12
+ ...(m.cc ? [`Cc: ${m.cc.map(person).join(", ")}`] : []),
13
+ ...(m.listId ? [`List-Id: News <${m.listId}>`, `List-Unsubscribe: <mailto:leave@${m.listId}>`] : []),
14
+ `Subject: ${m.subject}`,
15
+ `Message-ID: <${m.messageId}@example.test>`,
16
+ `Date: ${new Date(m.receivedAt).toUTCString()}`,
17
+ "MIME-Version: 1.0",
18
+ ...(m.extraHeaders ?? []),
19
+ ];
20
+ const body = Buffer.from(m.html ?? m.text ?? "").toString("base64");
21
+ if (!m.attachment)
22
+ return Buffer.from(`${[...headers, `Content-Type: ${type}; charset=utf-8`, "Content-Transfer-Encoding: base64"].join("\r\n")}\r\n\r\n${body}\r\n`);
23
+ const parts = [
24
+ `--b1\r\nContent-Type: ${type}; charset=utf-8\r\nContent-Transfer-Encoding: base64\r\n\r\n${body}`,
25
+ `--b1\r\nContent-Type: ${m.attachment.mime}\r\nContent-Disposition: attachment; filename="${m.attachment.name}"\r\n` +
26
+ `Content-Transfer-Encoding: base64\r\n\r\n${Buffer.from(m.attachment.content).toString("base64")}`,
27
+ ];
28
+ return Buffer.from(`${[...headers, 'Content-Type: multipart/mixed; boundary="b1"'].join("\r\n")}\r\n\r\n${parts.join("\r\n")}\r\n--b1--\r\n`);
29
+ };
30
+ const failure = (message) => new CliError("provider_error", message, { reason: "mail.source-failed" });
31
+ const labelsOf = (m) => [...new Set([...(m.labels ?? []), ...(m.flagged ? ["Starred"] : [])])].sort();
32
+ const listingOf = (m, headers, folder) => ({
33
+ uid: m.uid,
34
+ messageId: m.messageId,
35
+ threadId: m.threadId,
36
+ receivedAt: new Date(m.receivedAt).toISOString(),
37
+ size: rawOf(m).length,
38
+ labels: labelsOf(m),
39
+ seen: m.seen ?? false,
40
+ flagged: m.flagged ?? false,
41
+ ...(headers ? { headers: headersOf(Buffer.from(rawOf(m)).toString().split("\r\n\r\n")[0] ?? "") } : {}),
42
+ folder,
43
+ });
44
+ /**
45
+ * An in-memory mail source with Gmail's shape: ids and threads that outlive a folder, labels and flags, UIDVALIDITY,
46
+ * a trash a move reaches, and All Mail under the account's language. The sync tests and the adapter contract run on it.
47
+ */
48
+ export const fakeMailSource = (mailbox) => {
49
+ const all = () => mailbox.allMail ?? GMAIL_ALL_MAIL;
50
+ const trash = () => mailbox.trash ?? TRASH;
51
+ const inFolder = (folder) => {
52
+ if (![all(), trash(), "INBOX"].includes(folder))
53
+ throw failure(`Gmail has no such mailbox: ${folder}`);
54
+ return mailbox.messages.filter((m) => (folder === trash() ? m.folder === "trash" : m.folder !== "trash"));
55
+ };
56
+ const search = async (since, folder) => {
57
+ if (mailbox.searchFails)
58
+ throw failure(mailbox.searchFails);
59
+ const from = Date.UTC(since.getUTCFullYear(), since.getUTCMonth(), since.getUTCDate());
60
+ return {
61
+ validity: String(mailbox.validity ?? 12),
62
+ uids: inFolder(folder)
63
+ .filter((m) => Date.parse(m.receivedAt) >= from)
64
+ .map((m) => m.uid),
65
+ };
66
+ };
67
+ const failedOnce = new Set();
68
+ const failsNow = (uid) => {
69
+ if (!mailbox.failOnce?.includes(uid) || failedOnce.has(uid))
70
+ return false;
71
+ failedOnce.add(uid);
72
+ return true;
73
+ };
74
+ const count = (kind) => {
75
+ if (mailbox.calls)
76
+ mailbox.calls[kind] += 1;
77
+ };
78
+ return {
79
+ ...(mailbox.listBatch === undefined ? {} : { listBatch: mailbox.listBatch }),
80
+ ...(mailbox.fetchBatch === undefined ? {} : { fetchBatch: mailbox.fetchBatch }),
81
+ search: async (since, folder) => {
82
+ count("search");
83
+ return search(since, folder);
84
+ },
85
+ list: async (since, folder, options) => {
86
+ count("list");
87
+ const { uids } = options?.searched ?? (await search(since, folder));
88
+ if (uids.length > 0)
89
+ mailbox.asked?.push(uids.join(","));
90
+ if (options?.headers && mailbox.headersFail)
91
+ throw failure("the header fetch was refused");
92
+ const wanted = new Set(uids);
93
+ return inFolder(folder)
94
+ .filter((m) => wanted.has(m.uid) && !failsNow(m.uid))
95
+ .map((m) => listingOf(m, options?.headers ?? false, folder));
96
+ },
97
+ find: async (messageIds, folder, options) => {
98
+ for (const id of messageIds)
99
+ mailbox.asked?.push(`msgid:${id}`);
100
+ const found = inFolder(folder).filter((m) => messageIds.includes(m.messageId));
101
+ if (found.length > 0)
102
+ mailbox.asked?.push(found.map((m) => m.uid).join(","));
103
+ return found.map((m) => listingOf(m, options?.headers ?? false, folder));
104
+ },
105
+ move: async (messageIds, from, to) => {
106
+ if (mailbox.moveFails)
107
+ throw failure("UID MOVE did not complete");
108
+ const here = inFolder(from).filter((m) => messageIds.includes(m.messageId));
109
+ if (here.length === 0)
110
+ return { moved: [], missing: [...messageIds] };
111
+ for (const m of here) {
112
+ if (to === trash()) {
113
+ m.folder = "trash";
114
+ m.labels = (m.labels ?? []).filter((label) => label !== "Inbox");
115
+ }
116
+ else {
117
+ delete m.folder;
118
+ if (to === "INBOX" && !(m.labels ?? []).includes("Inbox"))
119
+ m.labels = ["Inbox", ...(m.labels ?? [])];
120
+ }
121
+ }
122
+ const moved = here.map((m) => m.messageId);
123
+ return { moved, missing: messageIds.filter((id) => !moved.includes(id)) };
124
+ },
125
+ folders: async () => ({ all: all(), trash: trash() }),
126
+ present: async (uids, folder) => inFolder(folder).flatMap((m) => (uids.includes(m.uid) ? [m.uid] : [])),
127
+ label: async (messageIds, folder, labels) => {
128
+ for (const m of inFolder(folder))
129
+ if (messageIds.includes(m.messageId))
130
+ m.labels = [...new Set([...(m.labels ?? []), ...labels])];
131
+ },
132
+ fetchRaw: async (items) => {
133
+ count("fetchRaw");
134
+ return items.map((item) => {
135
+ const m = mailbox.messages.find((each) => each.uid === item.uid);
136
+ if (m === undefined || mailbox.unreadable?.includes(item.uid) || failsNow(item.uid))
137
+ return failure(`email ${item.uid} was not read`);
138
+ return rawOf(m);
139
+ });
140
+ },
141
+ };
142
+ };
143
+ /**
144
+ * A snapshot source, as an mbox file is: every message since a date with no UIDs, one stamp that changes when the
145
+ * file does, and no search, find or folders. `archive.mails` and `archive.stamp` are read live.
146
+ */
147
+ export const fakeArchiveSource = (archive) => ({
148
+ list: async (since, folder) => archive.mails
149
+ .filter((m) => Date.parse(m.receivedAt) >= since.getTime())
150
+ .map((m) => ({ ...listingOf(m, true, folder), uid: "" })),
151
+ stamp: async () => ({ ...archive.stamp }),
152
+ fetchRaw: async (items) => items.map((item) => {
153
+ const m = archive.mails.find((each) => each.messageId === item.messageId);
154
+ return m === undefined ? failure(`email ${item.messageId} was not found`) : rawOf(m);
155
+ }),
156
+ });
@@ -0,0 +1,2 @@
1
+ export { CONTRACT_MAIL, type MailSourceFixture, type MailSyncFixture, mailSourceContract, mailSyncContract, } from "./contract.js";
2
+ export { type FakeMail, type FakeMailbox, type FakePerson, fakeArchiveSource, fakeMailSource, rawOf } from "./fake.js";
@@ -0,0 +1,2 @@
1
+ export { CONTRACT_MAIL, mailSourceContract, mailSyncContract, } from "./contract.js";
2
+ export { fakeArchiveSource, fakeMailSource, rawOf } from "./fake.js";
@@ -7,6 +7,8 @@ export interface RunError {
7
7
  class: string;
8
8
  code?: string;
9
9
  reason?: string;
10
+ /** The transport's own code (`NoConnection`, `ETIMEDOUT`, `UNAVAILABLE`), kept so a failed run says what broke. */
11
+ cause_code?: string;
10
12
  message?: string;
11
13
  /** Ours, not the messenger's: `messages.send`. */
12
14
  operation?: string;
@@ -10,6 +10,9 @@ export const errorDetailOf = (error, response) => {
10
10
  const thrown = error instanceof Error ? error : undefined;
11
11
  const code = isCliFailure(error) ? error.code : codeOf(error);
12
12
  const reason = typeof details.reason === "string" ? details.reason : undefined;
13
+ const causeCode = typeof details.cause_code === "string" && /^[A-Za-z0-9_.-]{1,40}$/.test(details.cause_code)
14
+ ? details.cause_code
15
+ : undefined;
13
16
  const message = thrown ? redactText(thrown.message) : undefined;
14
17
  const operation = stringOf(details.operation) ?? response?.operation;
15
18
  const status = numberOf(details.status) ?? response?.status;
@@ -30,6 +33,7 @@ export const errorDetailOf = (error, response) => {
30
33
  class: classOf(error),
31
34
  ...(code === undefined ? {} : { code }),
32
35
  ...(reason === undefined ? {} : { reason }),
36
+ ...(causeCode === undefined ? {} : { cause_code: causeCode }),
33
37
  ...(message ? { message } : {}),
34
38
  ...(operation === undefined ? {} : { operation }),
35
39
  ...(http ? { http } : {}),
@@ -19,6 +19,8 @@ export interface Hearing {
19
19
  unheard: Voice[];
20
20
  /** Why some were not heard — the first reason; the messages are still worth showing. */
21
21
  problem?: string;
22
+ /** Set under `--offline`: the command (`<app> <profile>`) that hears a message online, which `--offline` never does. */
23
+ offline?: string;
22
24
  }
23
25
  export declare const keyOf: ({ chatId, messageId }: Voice) => string;
24
26
  /** Telegram says `voice`; MAX says `audio` with Ogg in it. */
@@ -119,6 +119,16 @@ export const hearingMissing = (hearing, transcribe) => {
119
119
  return [{ field: "transcript", why: `not transcribed: ${hearing.problem}`, step: "messages.transcribe" }];
120
120
  if (hearing.unheard.length === 0)
121
121
  return [];
122
+ if (hearing.offline !== undefined) {
123
+ const commands = hearing.unheard.map(({ chatId, messageId }) => `${hearing.offline} messages transcribe ${chatId} ${messageId}`);
124
+ return [
125
+ {
126
+ field: "transcript",
127
+ why: `--offline reads only transcripts already stored; ${hearing.unheard.length} voice message(s) have none: ${commands.join("; ")}`,
128
+ step: "messages.transcribe",
129
+ },
130
+ ];
131
+ }
122
132
  const ids = hearing.unheard.map((voice) => voice.messageId).join(", ");
123
133
  return [
124
134
  {
@@ -13,10 +13,12 @@ export const PROJECTS_QUERY = {
13
13
  entity: "project",
14
14
  backend: "sql",
15
15
  fields: queryFieldsOf(project, {
16
- native: ["id", "key", "type", "organization_id", "scope", "created_at"],
16
+ native: ["id", "key", "name", "kind", "type", "organization_id", "scope", "created_at"],
17
17
  examples: {
18
18
  id: "id:1",
19
19
  key: "key:SMP",
20
+ name: "name:*tart*",
21
+ kind: "kind:inbox",
20
22
  type: "type:work",
21
23
  organization_id: "organization_id:1",
22
24
  scope: "scope:work",
@@ -45,6 +45,8 @@ export interface PeopleStore extends AccountPeople {
45
45
  /** The person a `person:<uid>` reference names. */
46
46
  personSeenInAccounts(uid: string, accountIds: readonly number[]): Promise<boolean>;
47
47
  personByUid(uid: string): Promise<PersonRecord | undefined>;
48
+ /** The person marked as the owner, with every identity linked to them; `undefined` when none is. */
49
+ owner(): Promise<PersonRecord | undefined>;
48
50
  /** Accepted facts, the owner's notes and open tasks about them; tasks of an account only if `accounts` holds it. */
49
51
  preparation(uid: string, options: {
50
52
  accounts: readonly AccountKey[];
@@ -1,4 +1,6 @@
1
+ import { CliError } from "@wirecat/cli-base";
1
2
  import { keysOver } from "./keys.js";
3
+ import { ownerPerson } from "./sqlite/actors.js";
2
4
  import * as identities from "./sqlite/identities.js";
3
5
  import { linkToOwner } from "./sqlite/own-identities.js";
4
6
  import * as personLinks from "./sqlite/person-links.js";
@@ -59,6 +61,16 @@ export const peopleStoreOver = (context, keyed) => {
59
61
  personOf: async (identity) => personLinks.personOf(context, identity),
60
62
  personSeenInAccounts: async (uid, accountIds) => personSeenInAccounts(context, uid, accountIds),
61
63
  personByUid: async (uid) => personLinks.personByUid(context, uid),
64
+ owner: async () => {
65
+ try {
66
+ return personLinks.personByUid(context, String(ownerPerson(database).id));
67
+ }
68
+ catch (error) {
69
+ if (error instanceof CliError && error.code === "configuration_error")
70
+ return undefined;
71
+ throw error;
72
+ }
73
+ },
62
74
  preparation: async (uid, options) => personPreparation(context, uid, options),
63
75
  linkIdentities: async (person, other, options) => {
64
76
  return write(database, () => personLinks.linkIdentities(context, person, other, options));
@@ -1,4 +1,4 @@
1
- import type { QueryAst } from "@wirecat/cli-base/query";
1
+ import { type QueryAst } from "@wirecat/cli-base/query";
2
2
  import type { AccountKey } from "../types.js";
3
3
  import { RELATION_KINDS } from "./link-kinds.js";
4
4
  import { type MeetingProjectsStore } from "./meeting-projects.js";
@@ -78,6 +78,8 @@ export interface Project {
78
78
  ref: string;
79
79
  key: string;
80
80
  name: string;
81
+ /** `inbox`: the per-account project that holds an account's tasks; listed only when asked for. */
82
+ kind: "project" | "inbox";
81
83
  type: (typeof PROJECT_TYPES)[number];
82
84
  organizationId: string | null;
83
85
  scope: (typeof SCOPES)[number];
@@ -1,4 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { walkQuery } from "@wirecat/cli-base/query";
2
3
  import { parseLocator } from "../../domain/locator.js";
3
4
  import { formatReference, parseReference } from "../../domain/references.js";
4
5
  import { normalizeTag } from "../../domain/tags.js";
@@ -11,7 +12,7 @@ import { meetingProjectsStoreOver } from "./meeting-projects.js";
11
12
  import { insertLink, linkOf } from "./notes.js";
12
13
  import { queryFilter } from "./query-filter.js";
13
14
  import { addTags as addThingTags, canonicalTagNamed, createTag, labelsOf, liveTagging, removeTags as removeThingTags, setMainTopic, } from "./tags.js";
14
- import { TASK_KEY_PREFIXES } from "./task-keys.js";
15
+ import { INBOX_KEY_LENGTH, TASK_KEY_PREFIXES } from "./task-keys.js";
15
16
  import { inboxKey, taskAccountOf, taskIdOf, taskRowOf } from "./tasks.js";
16
17
  import { referenceOfThing, stateOfThing, storedThing, thingOf } from "./things.js";
17
18
  import { write } from "./transaction.js";
@@ -199,6 +200,7 @@ export const knowledgeStoreOver = (context) => {
199
200
  ref: `project:${row.id}`,
200
201
  key: String(row.key),
201
202
  name: String(row.name),
203
+ kind: String(row.key).length === INBOX_KEY_LENGTH ? "inbox" : "project",
202
204
  type: row.type,
203
205
  organizationId: row.organization_id == null ? null : String(row.organization_id),
204
206
  scope: row.scope,
@@ -489,17 +491,20 @@ export const knowledgeStoreOver = (context) => {
489
491
  return projectOf(row);
490
492
  }),
491
493
  projects: async (query) => {
494
+ const asksKind = query !== undefined && walkQuery(query.root).some((node) => node.field === "kind");
492
495
  const where = query &&
493
496
  queryFilter(query, {
494
497
  id: { type: "kw", sql: "CAST(id AS TEXT)" },
495
498
  key: { type: "kw", sql: "key" },
499
+ name: { type: "kw", sql: "name" },
500
+ kind: { type: "enum", sql: `CASE WHEN length(key) = ${INBOX_KEY_LENGTH} THEN 'inbox' ELSE 'project' END` },
496
501
  type: { type: "enum", sql: "type" },
497
502
  organization_id: { type: "kw", sql: "CAST(organization_id AS TEXT)" },
498
503
  scope: { type: "enum", sql: "scope" },
499
504
  created_at: { type: "time", sql: "created_at" },
500
505
  });
501
506
  return database
502
- .prepare(`SELECT * FROM projects WHERE deleted_at IS NULL${where ? ` AND (${where.sql})` : ""} ORDER BY name, id LIMIT 500`)
507
+ .prepare(`SELECT * FROM projects WHERE deleted_at IS NULL${asksKind ? "" : ` AND length(key) <> ${INBOX_KEY_LENGTH}`}${where ? ` AND (${where.sql})` : ""} ORDER BY name, id LIMIT 500`)
503
508
  .all(...(where?.params ?? []))
504
509
  .map(projectOf);
505
510
  },
@@ -8,6 +8,7 @@ export declare const TASK_KEY_PREFIXES: Readonly<Record<TaskKind, string>>;
8
8
  */
9
9
  export declare const typedKey: (database: CacheDatabase, kind: TaskKind) => string;
10
10
  /** One inbox project per task account (`telegram:100`): derived from the account text alone, no `-` in it. */
11
+ export declare const INBOX_KEY_LENGTH = 10;
11
12
  export declare const inboxKey: (account: string) => string;
12
13
  /** An inbox made before its messenger saved the account learns it here, not only on its next task. */
13
14
  export declare const linkInbox: (database: CacheDatabase, account: string, accountId: number) => void;
@@ -22,6 +22,7 @@ export const typedKey = (database, kind) => {
22
22
  }
23
23
  };
24
24
  /** One inbox project per task account (`telegram:100`): derived from the account text alone, no `-` in it. */
25
+ export const INBOX_KEY_LENGTH = 10;
25
26
  export const inboxKey = (account) => `IN${createHash("sha256").update(account).digest("hex").slice(0, 8).toUpperCase()}`;
26
27
  /** An inbox made before its messenger saved the account learns it here, not only on its next task. */
27
28
  export const linkInbox = (database, account, accountId) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.471.0",
3
+ "version": "0.473.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",
@@ -125,6 +125,14 @@
125
125
  "types": "./dist/calendar/testing/index.d.ts",
126
126
  "default": "./dist/calendar/testing/index.js"
127
127
  },
128
+ "./mail": {
129
+ "types": "./dist/mail/index.d.ts",
130
+ "default": "./dist/mail/index.js"
131
+ },
132
+ "./mail/testing": {
133
+ "types": "./dist/mail/testing/index.d.ts",
134
+ "default": "./dist/mail/testing/index.js"
135
+ },
128
136
  "./models": {
129
137
  "types": "./dist/models/index.d.ts",
130
138
  "default": "./dist/models/index.js"
@@ -200,6 +208,7 @@
200
208
  "dictionary-en": "4.0.0",
201
209
  "dictionary-es": "4.0.0",
202
210
  "dictionary-ru": "3.0.0",
211
+ "disposable-email-domains-js": "1.26.0",
203
212
  "echarts": "6.1.0",
204
213
  "fflate": "0.8.3",
205
214
  "iconv-lite": "0.7.3",
@@ -212,10 +221,12 @@
212
221
  "mdast-util-gfm": "3.1.0",
213
222
  "micromark-extension-gfm": "3.0.0",
214
223
  "ogg-opus-decoder": "1.7.5",
224
+ "postal-mime": "4.1.4",
215
225
  "psl": "1.15.0",
216
226
  "qrcode-generator": "2.0.4",
217
227
  "sherpa-onnx": "2.0.0",
218
228
  "string-width": "8.3.0",
229
+ "tldts": "7.4.19",
219
230
  "unpdf": "1.9.1",
220
231
  "valibot": "1.5.0",
221
232
  "word-extractor": "1.0.4",