@wirecat/cli-core 0.554.0 → 0.555.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 (45) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/cli/messenger/catch-up-answers.d.ts +50 -1
  3. package/dist/cli/messenger/catch-up-answers.js +2 -1
  4. package/dist/cli/messenger/catch-up.d.ts +6 -2
  5. package/dist/cli/messenger/catch-up.js +11 -2
  6. package/dist/cli/messenger/conversation-answers.d.ts +3 -0
  7. package/dist/cli/messenger/inbox.js +1 -1
  8. package/dist/cli/messenger/message-answers.d.ts +3 -0
  9. package/dist/cli/messenger/message-writes.d.ts +6 -0
  10. package/dist/cli/messenger/points.d.ts +9 -4
  11. package/dist/cli/messenger/points.js +10 -4
  12. package/dist/cli/messenger/review.js +1 -1
  13. package/dist/domain/models.d.ts +4 -0
  14. package/dist/entities/contact-context.d.ts +6 -0
  15. package/dist/entities/conversation-results.d.ts +6 -0
  16. package/dist/entities/evidence.d.ts +24 -0
  17. package/dist/entities/index.d.ts +263 -3
  18. package/dist/entities/index.js +1 -0
  19. package/dist/entities/listener.d.ts +12 -0
  20. package/dist/entities/message-context.d.ts +9 -0
  21. package/dist/entities/message-write.d.ts +12 -0
  22. package/dist/entities/message.d.ts +6 -0
  23. package/dist/entities/message.js +6 -0
  24. package/dist/entities/store-records.d.ts +3 -0
  25. package/dist/mail/cli/index.d.ts +2 -1
  26. package/dist/mail/cli/index.js +4 -1
  27. package/dist/mail/cli/review.d.ts +24 -0
  28. package/dist/mail/cli/review.js +204 -0
  29. package/dist/mail/index.d.ts +2 -0
  30. package/dist/mail/index.js +2 -0
  31. package/dist/mail/owed.d.ts +11 -0
  32. package/dist/mail/owed.js +14 -0
  33. package/dist/mail/review.d.ts +37 -0
  34. package/dist/mail/review.js +118 -0
  35. package/dist/services/agenda-mail.d.ts +15 -0
  36. package/dist/services/agenda-mail.js +66 -0
  37. package/dist/services/agenda.d.ts +4 -0
  38. package/dist/services/agenda.js +17 -7
  39. package/dist/store/exports.d.ts +1 -0
  40. package/dist/store/exports.js +1 -0
  41. package/dist/store/sqlite/email-activity.d.ts +62 -0
  42. package/dist/store/sqlite/email-activity.js +71 -0
  43. package/dist/store/sqlite/emails.d.ts +7 -3
  44. package/dist/store/sqlite/emails.js +4 -8
  45. package/package.json +1 -1
@@ -0,0 +1,62 @@
1
+ import type { CacheDatabase } from "../driver.js";
2
+ import type { Where } from "./query-filter.js";
3
+ /** What decides whose turn a thread is, read from its newest email that is not a draft; never its text. */
4
+ export interface NewestEmail {
5
+ externalId: string;
6
+ /** ms: when it was received, or sent when that is unknown. */
7
+ at: number;
8
+ outgoing: boolean;
9
+ /** The mailbox's own address is in To or Cc. */
10
+ addressed: boolean;
11
+ /** Filed in the mailbox of mail sent to many. */
12
+ bulk: boolean;
13
+ /** Carries a List-Id. */
14
+ list: boolean;
15
+ /** Recorded by an envelope rule, without its text. */
16
+ envelope: boolean;
17
+ /** Its sender's mail looks like spam, a scam or a robot (the sender score). */
18
+ demoted: boolean;
19
+ }
20
+ /** One thread with mail after a time: `tasks agenda` and `review` read this, never the emails' text. */
21
+ export interface ThreadActivity {
22
+ /** The thread's external id: what a `chat:email/…` reference names. */
23
+ threadId: string;
24
+ subject: string | null;
25
+ /** ms. */
26
+ lastEmailAt: number;
27
+ emails: number;
28
+ /** `null` when every email of the thread is a draft. */
29
+ newest: NewestEmail | null;
30
+ }
31
+ /** What a reader sorts mail by, per email: never its text. */
32
+ export interface EmailMarks {
33
+ /** Filed in the mailbox of mail sent to many. */
34
+ bulk: boolean;
35
+ listId: string | null;
36
+ /** The mail rule that decided how it is stored, by its id in `rules list`. */
37
+ ruleId: number | null;
38
+ }
39
+ export interface MailActivityStore {
40
+ /** Each live email's marks, by its Message-ID; one the store does not hold is left out. */
41
+ emailMarks(accountId: number, externalIds: readonly string[]): Promise<Map<string, EmailMarks>>;
42
+ /**
43
+ * Each live thread of the account whose newest email is after `since` (ms), newest first, with the facts of that
44
+ * email. `where` is a `tasks agenda` query compiled over `MAIL_AGENDA_COLUMNS`.
45
+ */
46
+ threadActivity(accountId: number, since: number, where?: Where): Promise<ThreadActivity[]>;
47
+ }
48
+ /** `tasks agenda` fields over `threadActivity`'s threads: an email thread counts as a dialog. */
49
+ export declare const MAIL_AGENDA_COLUMNS: {
50
+ readonly chat_kind: {
51
+ readonly type: "enum";
52
+ readonly sql: "'dialog'";
53
+ };
54
+ };
55
+ export declare const threadActivity: (database: CacheDatabase, accountId: number, since: number, where?: Where) => ThreadActivity[];
56
+ /** A thread's live emails, oldest first; `window` keeps those received in (after, until], in ms. */
57
+ export declare const threadEmailIds: (database: CacheDatabase, accountId: number, thread: string, { after, until }?: {
58
+ after?: number;
59
+ until?: number;
60
+ }) => number[];
61
+ export declare const emailMarks: (database: CacheDatabase, accountId: number, externalIds: readonly string[]) => Map<string, EmailMarks>;
62
+ export declare const mailActivity: (database: CacheDatabase) => MailActivityStore;
@@ -0,0 +1,71 @@
1
+ import { int, str } from "./events.js";
2
+ /** `tasks agenda` fields over `threadActivity`'s threads: an email thread counts as a dialog. */
3
+ export const MAIL_AGENDA_COLUMNS = { chat_kind: { type: "enum", sql: "'dialog'" } };
4
+ // The thread's own time is the Date header where sync stored one, so a thread is picked by it; the newest email is
5
+ // picked by when it arrived, which is what an answer follows.
6
+ const ACTIVITY = `
7
+ SELECT t.external_id AS thread, t.subject, t.last_email_at, t.emails_count,
8
+ e.external_id, coalesce(e.received_at, e.sent_at) AS at,
9
+ coalesce(e.outgoing, lower(e.from_address) = lower(a.external_id)) AS outgoing,
10
+ EXISTS (SELECT 1 FROM email_recipients r WHERE r.email_id = e.id AND r.role IN ('to', 'cc')
11
+ AND lower(r.address) = lower(a.external_id)) AS addressed,
12
+ EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
13
+ WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk,
14
+ coalesce(json_extract(e.headers, '$."list-id"'), json_extract(e.metadata, '$.listId')) IS NOT NULL AS list,
15
+ EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
16
+ WHERE m.email_id = e.id AND b.kind = 'envelope') AS envelope,
17
+ EXISTS (SELECT 1 FROM account_identities ai WHERE ai.account_id = t.account_id
18
+ AND ai.identity_id = e.from_identity_id AND ai.mail_demoted = 1) AS demoted
19
+ FROM email_threads t JOIN accounts a ON a.id = t.account_id
20
+ LEFT JOIN emails e ON e.id = (SELECT f.id FROM emails f WHERE f.email_thread_id = t.id AND f.deleted_at IS NULL
21
+ AND coalesce(f.draft, 0) = 0 ORDER BY coalesce(f.received_at, f.sent_at) DESC, f.id DESC LIMIT 1)
22
+ WHERE t.account_id = ? AND t.deleted_at IS NULL AND t.last_email_at > ?`;
23
+ export const threadActivity = (database, accountId, since, where) => database
24
+ .prepare(`${ACTIVITY}${where ? ` AND (${where.sql})` : ""} ORDER BY t.last_email_at DESC, t.id DESC`)
25
+ .all(accountId, since, ...(where?.params ?? []))
26
+ .map((row) => ({
27
+ threadId: String(row.thread),
28
+ subject: str(row.subject),
29
+ lastEmailAt: Number(row.last_email_at),
30
+ emails: Number(row.emails_count),
31
+ newest: row.external_id == null
32
+ ? null
33
+ : {
34
+ externalId: String(row.external_id),
35
+ at: int(row.at) ?? Number(row.last_email_at),
36
+ outgoing: Boolean(row.outgoing),
37
+ addressed: Boolean(row.addressed),
38
+ bulk: Boolean(row.bulk),
39
+ list: Boolean(row.list),
40
+ envelope: Boolean(row.envelope),
41
+ demoted: Boolean(row.demoted),
42
+ },
43
+ }));
44
+ /** A thread's live emails, oldest first; `window` keeps those received in (after, until], in ms. */
45
+ export const threadEmailIds = (database, accountId, thread, { after, until } = {}) => database
46
+ .prepare(`SELECT e.id FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
47
+ WHERE t.account_id = ? AND t.external_id = ? AND e.deleted_at IS NULL
48
+ AND coalesce(e.received_at, e.sent_at, 0) > coalesce(?, -1)
49
+ AND coalesce(e.received_at, e.sent_at, 0) <= coalesce(?, 9e15)
50
+ ORDER BY coalesce(e.sent_at, e.received_at), e.id`)
51
+ .all(accountId, thread, after ?? null, until ?? null)
52
+ .map(({ id }) => Number(id));
53
+ const MARKS = `
54
+ SELECT e.mail_rule_id, coalesce(json_extract(e.headers, '$."list-id"'), json_extract(e.metadata, '$.listId')) AS list_id,
55
+ EXISTS (SELECT 1 FROM email_mailboxes m JOIN mailboxes b ON b.id = m.mailbox_id
56
+ WHERE m.email_id = e.id AND b.kind = 'bulk') AS bulk
57
+ FROM emails e WHERE e.account_id = ? AND e.external_id = ? AND e.deleted_at IS NULL`;
58
+ export const emailMarks = (database, accountId, externalIds) => {
59
+ const one = database.prepare(MARKS);
60
+ const marks = new Map();
61
+ for (const id of externalIds) {
62
+ const row = one.get(accountId, id);
63
+ if (row)
64
+ marks.set(id, { bulk: Boolean(row.bulk), listId: str(row.list_id), ruleId: int(row.mail_rule_id) });
65
+ }
66
+ return marks;
67
+ };
68
+ export const mailActivity = (database) => ({
69
+ emailMarks: async (accountId, externalIds) => emailMarks(database, accountId, externalIds),
70
+ threadActivity: async (accountId, since, where) => threadActivity(database, accountId, since, where),
71
+ });
@@ -3,6 +3,7 @@ import type { Attachment, Metadata } from "../../meetings/index.js";
3
3
  import type { Stemmer, Stemmers } from "../../search/stem.js";
4
4
  import type { CacheDatabase } from "../driver.js";
5
5
  import type { ConversationSummary, StoredHit } from "../types.js";
6
+ import { type MailActivityStore } from "./email-activity.js";
6
7
  import { type EmailLocationInput, type FolderEmail, type PartialMailStore } from "./email-locations.js";
7
8
  import { type MailRulesStore } from "./mail-rules.js";
8
9
  import { type AttachmentInput } from "./meetings.js";
@@ -205,7 +206,7 @@ export interface SenderScore {
205
206
  demoted?: boolean;
206
207
  }
207
208
  /** Mail in tables of its own: threads, emails, recipients and mailboxes, keyed by account and Message-ID. */
208
- export interface MailStore extends PartialMailStore {
209
+ export interface MailStore extends PartialMailStore, MailActivityStore {
209
210
  saveThread(input: ThreadSave): Promise<ThreadDetails>;
210
211
  /** Several threads and sync state in one transaction: a slice of mail commits with its resume point, or not at all. */
211
212
  saveThreads(inputs: ThreadSave[], states?: SyncStateSave[]): Promise<ThreadDetails[]>;
@@ -268,8 +269,11 @@ export interface MailStore extends PartialMailStore {
268
269
  }[]>;
269
270
  /** Live emails of one account by Message-ID, each placed in its thread; an unknown one is left out. */
270
271
  inThreads(accountId: number, externalIds: string[]): Promise<EmailInThread[]>;
271
- /** A thread's live emails, oldest first, as search hits: whole text, people, labels and locator. */
272
- threadHits(accountId: number, threadExternalId: string): Promise<StoredHit[]>;
272
+ /** A thread's live emails, oldest first, as search hits; `window` keeps those received in (after, until], in ms. */
273
+ threadHits(accountId: number, thread: string, window?: {
274
+ after?: number;
275
+ until?: number;
276
+ }): Promise<StoredHit[]>;
273
277
  /** The emails of one account nearest in meaning to `query`, each by its best chunk, best first. */
274
278
  nearest(model: string, query: Float32Array, options: {
275
279
  accountId: number;
@@ -1,6 +1,7 @@
1
1
  import { formatLocator } from "../../domain/locator.js";
2
2
  import { formatReference } from "../../domain/references.js";
3
3
  import { fold, normalize } from "../normalize.js";
4
+ import { mailActivity, threadEmailIds } from "./email-activity.js";
4
5
  import { chunkAttachments, drainEmailIndex, dropAttachmentChunks, MAIL_PIECES, queueThreads, threadPieces, titleOf, } from "./email-index.js";
5
6
  import { backfillSentAt, folderEmails, forgetLocations, locate, partialMail, setLocations, storedWhole, } from "./email-locations.js";
6
7
  import { markEmailsDeleted, markEmailsRestored, recountEmailThread as recount, setEmailFlags } from "./email-state.js";
@@ -472,6 +473,7 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
472
473
  }));
473
474
  },
474
475
  ...partialMail(database),
476
+ ...mailActivity(database),
475
477
  async emailFacts(filter = {}) {
476
478
  const [where, params] = emailWhere(filter);
477
479
  return database
@@ -631,14 +633,8 @@ export const mailStoreOver = ({ database, orm, now }, stemmerFor = stemmerCache(
631
633
  return place ? [{ ...place, score }] : [];
632
634
  });
633
635
  },
634
- async threadHits(accountId, threadExternalId) {
635
- const ids = database
636
- .prepare(`SELECT e.id FROM emails e JOIN email_threads t ON t.id = e.email_thread_id
637
- WHERE t.account_id = ? AND t.external_id = ? AND e.deleted_at IS NULL
638
- ORDER BY coalesce(e.sent_at, e.received_at), e.id`)
639
- .all(accountId, threadExternalId)
640
- .map(({ id }) => Number(id));
641
- return emailHitsByPk(database, ids);
636
+ async threadHits(accountId, thread, window) {
637
+ return emailHitsByPk(database, threadEmailIds(database, accountId, thread, window));
642
638
  },
643
639
  async inThreads(accountId, externalIds) {
644
640
  const id = database.prepare("SELECT id FROM emails WHERE account_id = ? AND external_id = ? AND deleted_at IS NULL");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.554.0",
3
+ "version": "0.555.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",