@wirecat/cli-core 0.546.0 → 0.547.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 +152 -0
- package/dist/cli/messenger/history-query.d.ts +10 -4
- package/dist/cli/messenger/history-query.js +87 -31
- package/dist/cli/messenger/messages-command.js +10 -4
- package/dist/cli/messenger/messages-comments-command.js +4 -2
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/option-hints.d.ts +6 -0
- package/dist/cli/option-hints.js +15 -0
- package/dist/errors/reasons.js +13 -1
- package/dist/mail/cli/index.js +4 -4
- package/dist/mail/index.d.ts +2 -0
- package/dist/mail/index.js +2 -0
- package/dist/mail/partial.d.ts +34 -0
- package/dist/mail/partial.js +94 -0
- package/dist/mail/server-search.js +17 -9
- package/dist/mail/source.d.ts +13 -2
- package/dist/mail/source.js +7 -1
- package/dist/mail/sync-types.d.ts +112 -0
- package/dist/mail/sync-types.js +1 -0
- package/dist/mail/sync.d.ts +4 -106
- package/dist/mail/sync.js +49 -4
- package/dist/mail/testing/fake.d.ts +3 -1
- package/dist/mail/testing/fake.js +10 -2
- package/dist/services/deps.d.ts +0 -2
- package/dist/services/deps.js +0 -2
- package/dist/services/documents/ingestion.js +1 -1
- package/dist/services/documents/knowledge.d.ts +10 -6
- package/dist/services/documents/knowledge.js +25 -11
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/messages.d.ts +2 -0
- package/dist/services/messages.js +25 -23
- package/dist/services/proposal-tasks.d.ts +3 -2
- package/dist/services/proposal-tasks.js +5 -2
- package/dist/services/proposals/ids.d.ts +5 -0
- package/dist/services/proposals/ids.js +43 -0
- package/dist/services/proposals/index.d.ts +3 -0
- package/dist/services/proposals/index.js +2 -0
- package/dist/services/proposals/kinds/action.d.ts +7 -0
- package/dist/services/proposals/kinds/action.js +45 -0
- package/dist/services/proposals/kinds/fact.d.ts +4 -0
- package/dist/services/proposals/kinds/fact.js +43 -0
- package/dist/services/proposals/kinds/findings.d.ts +7 -0
- package/dist/services/proposals/kinds/findings.js +75 -0
- package/dist/services/proposals/kinds/key-document.d.ts +9 -0
- package/dist/services/proposals/kinds/key-document.js +75 -0
- package/dist/services/proposals/kinds/memory.d.ts +4 -0
- package/dist/services/proposals/kinds/memory.js +42 -0
- package/dist/services/proposals/kinds/relationship.d.ts +10 -0
- package/dist/services/proposals/kinds/relationship.js +82 -0
- package/dist/services/proposals/kinds/shared.d.ts +25 -0
- package/dist/services/proposals/kinds/shared.js +49 -0
- package/dist/services/proposals/registry.d.ts +9 -0
- package/dist/services/proposals/registry.js +129 -0
- package/dist/services/proposals/types.d.ts +85 -0
- package/dist/services/proposals/types.js +1 -0
- package/dist/services/server-search.d.ts +7 -4
- package/dist/services/server-search.js +5 -2
- package/dist/services/tasks.js +8 -4
- package/dist/store/decisions.d.ts +65 -0
- package/dist/store/decisions.js +55 -0
- package/dist/store/evidence-findings.d.ts +6 -6
- package/dist/store/index.d.ts +4 -3
- package/dist/store/index.js +2 -1
- package/dist/store/meeting-findings.d.ts +2 -0
- package/dist/store/messages.d.ts +4 -9
- package/dist/store/sqlite/actors.d.ts +13 -2
- package/dist/store/sqlite/actors.js +24 -1
- package/dist/store/sqlite/email-locations.d.ts +16 -0
- package/dist/store/sqlite/email-locations.js +26 -0
- package/dist/store/sqlite/emails.d.ts +6 -2
- package/dist/store/sqlite/emails.js +2 -1
- package/dist/store/sqlite/evidence-findings.js +13 -3
- package/dist/store/sqlite/facts.d.ts +16 -8
- package/dist/store/sqlite/facts.js +48 -15
- package/dist/store/sqlite/findings-core.d.ts +2 -1
- package/dist/store/sqlite/findings-core.js +4 -4
- package/dist/store/sqlite/knowledge.d.ts +43 -7
- package/dist/store/sqlite/knowledge.js +55 -15
- package/dist/store/sqlite/meeting-findings.js +6 -6
- package/dist/store/sqlite/memories.d.ts +12 -8
- package/dist/store/sqlite/memories.js +33 -10
- package/dist/store/sqlite/notes.d.ts +5 -1
- package/dist/store/sqlite/notes.js +18 -2
- package/dist/store/sqlite/open-store.d.ts +4 -1
- package/dist/store/sqlite/open-store.js +8 -2
- package/dist/store/sqlite/open.d.ts +3 -0
- package/dist/store/sqlite/proposed-actions.d.ts +21 -8
- package/dist/store/sqlite/proposed-actions.js +29 -9
- package/dist/store/sqlite/reads.d.ts +13 -2
- package/dist/store/sqlite/reads.js +8 -4
- package/dist/store/sqlite/relation-proposals.d.ts +34 -0
- package/dist/store/sqlite/relation-proposals.js +114 -0
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,158 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.547.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **A server's refused SEARCH fails that folder, not the run, and proves nothing.** A mail source throws
|
|
15
|
+
`mail.search-refused` (`SEARCH_REFUSED`, `searchRefused`) when the server answers SEARCH with NO or BAD. `syncMail`
|
|
16
|
+
reads the other folders, answers `state: "partial"` with `refusedFolders`, and for the refused folder marks no mail
|
|
17
|
+
deleted, changes no membership, moves no resume point and leaves `mail_coverage` as it was. Before, a mail source that
|
|
18
|
+
read the refusal as "nothing found" made the folder look empty.
|
|
19
|
+
- **Server search reads big mail text-first, and the next sync completes it.** A candidate is read up to
|
|
20
|
+
`SERVER_BOUNDS.textBytes` (256 KiB, `fetchRaw(items, { bytes })`); one cut short is stored with its text and no
|
|
21
|
+
attachments, marked `metadata.partial`, and `meta.issues` says `search.server-partial`. `syncMail` then reads up to
|
|
22
|
+
100 partial emails whole and clears the mark (`partial: { completed, left }`; `completePartial: false` skips it).
|
|
23
|
+
`MailStore.partialEmails` lists them. No store migration: the mark is a JSON key older builds ignore.
|
|
24
|
+
- **`readClientFile` (from `@wirecat/cli-core/oauth`) reads a user's own Google Desktop OAuth client file** (the
|
|
25
|
+
`{ "installed": { client_id, client_secret } }` JSON from Google's console). It refuses a file other users can read
|
|
26
|
+
(`sign-in.client-file-open`) and a file of the wrong shape (`sign-in.client-file-invalid`), and never puts the file's
|
|
27
|
+
contents in an error. gcal, drive and mail each kept a copy of this reader.
|
|
28
|
+
- **`openBrowser(url, browser?)` is exported from `@wirecat/cli-core/cli`**, so a CLI that does not start through
|
|
29
|
+
`startCli` opens the sign-in page the same way: `open`, `rundll32` or `xdg-open`, or the named browser.
|
|
30
|
+
- **iCalendar feeds as read-only calendars.** With `deps.feeds` (`icsFeeds`), `calendars add <url> [--title]` follows a
|
|
31
|
+
public holiday calendar, a sports schedule or a published `.ics` address, and `calendars remove <calendar>` stops.
|
|
32
|
+
`sync` reads each feed after the provider (`syncWithFeeds`), so its events show in `events list`,
|
|
33
|
+
`search events`, `overlapping:` and `events remind`. Repeating events are expanded here (`expandSeries`: RRULE in
|
|
34
|
+
the event's own zone, RDATE, EXDATE and overrides). The address is a secret input, kept by the CLI and never
|
|
35
|
+
printed; a fetch follows redirects only to https and stops at 10 MB and 30 s. `readIcs` takes
|
|
36
|
+
`{ overrides: true }` and returns the calendar's `name`. See `docs/dev/CALENDARS.md`, "Feeds".
|
|
37
|
+
- **History limits for pushed history, per profile.** A messenger whose history is pushed (`history: "store"`,
|
|
38
|
+
WhatsApp) saves one-to-one chats back `history_dialog_days` (1095 by default), groups of more than two people back
|
|
39
|
+
`history_group_days` (365) and nothing of a group above `history_group_members_max` people (50); at most 1095 days.
|
|
40
|
+
The chat row stays, so the chat is still listed. Live messages, `messages sync` and what the store already holds are
|
|
41
|
+
never dropped. `messages sync <chat> --full-history` fetches a chat and keeps its pushed history in full from then on
|
|
42
|
+
(`history_included` in the answer). `ChatsStore` gains `historySkips`, `historyShapes`, `recordHistorySkips` and
|
|
43
|
+
`setHistoryIncluded`; `Saving` takes `historyLimits`.
|
|
44
|
+
- **Helpers for reading money and questions in a chat, for the project status work.** `amountsOf(text)` returns
|
|
45
|
+
every amount in a text, in order (`amountOf` is its first). `MONEY_REQUEST`, `MONEY_DONE` and `PROMISE` word lists
|
|
46
|
+
(ru, en, es) with `moneyClassesOf(text)` say which a text uses. `questions(messages, { asker: "owner" })` lists the
|
|
47
|
+
owner's own questions and whether anyone but the owner answered them, by a reply or by speaking next; the default,
|
|
48
|
+
`"others"`, is what it did before. `linkedChatsOf` is exported, now with each link's `role`, and so is
|
|
49
|
+
`AGENDA_DEFAULTS`, the silence `tasks agenda` waits before it lists a chat.
|
|
50
|
+
- **A command's own history handler learns whether each batch reached the store**: `HistoryBatchHandler` takes
|
|
51
|
+
`saved`, a promise of the same `true` or `false` the messenger gets, so a link can count only what was kept.
|
|
52
|
+
- **Every messenger command that takes a chat or a message also takes the reference other WireCat CLIs print**:
|
|
53
|
+
`chat:<provider>/<account>/<chat>` wherever a chat is named (`messages list chat:max/34871122/63488582`, bot
|
|
54
|
+
commands too), and `msg:<provider>/<account>/<chat>/<message>` wherever a chat or a message is, online and offline. A person lookup takes `contact:<provider>/<id>`. A reference of another
|
|
55
|
+
account is refused, naming the account it belongs to and the profiles of this CLI the store saw on it; it never
|
|
56
|
+
switches account. One of another messenger is refused with the command that opens it (`open it with tg <profile>
|
|
57
|
+
messages list chat:telegram/…`), and another kind (`document:413` given as a chat) with what the argument takes.
|
|
58
|
+
- **`event:<id>` and `meeting:<account>/<meeting>[/<transcript>[/<cue>]]` parse with `parseReference`** and
|
|
59
|
+
`canonicalReference`, as every other kind: `event:N` was printed by the calendar and person answers and could not be
|
|
60
|
+
read back. The kinds are one table, `REF_KINDS`; `looksLikeReference(text)` tells a reference from a title such as
|
|
61
|
+
`Note: plan`. Reference errors carry a reason: `ref.malformed`, `ref.unknown-kind` (with the kind meant:
|
|
62
|
+
`"doc:413" … did you mean document:413?`), `ref.wrong-kind`, `ref.wrong-provider`, `ref.wrong-account`.
|
|
63
|
+
- **`messages list` reads a window bounded on both sides, offline too.** The query takes a start and an end,
|
|
64
|
+
`'date>=2026-10-01 date<2026-10-20'` or `'date:[2026-10-01 TO 2026-10-19]'`, and `id>` with `date<`. A start reads
|
|
65
|
+
forward, oldest first, and stops at the end; an end alone reads back from it, newest page first, as before.
|
|
66
|
+
`--offline` and a messenger whose history is read from the store (WhatsApp) now answer both ways from the store
|
|
67
|
+
instead of refusing. A forward cursor carries the end, `after:<id>~<epoch ms>`, so the next page stops at the same
|
|
68
|
+
place. `--since`, `--since-time`, `--until`, `--until-time`, `--after-time` and `--before-time` on `messages list`
|
|
69
|
+
fail with the query form to type instead. The store's `messages()` window takes `after` (a message or a moment)
|
|
70
|
+
and `until`.
|
|
71
|
+
- **One proposal inbox behind one check: `proposalRegistry(store, policy)`, `ProposalKind`, `parseProposalIds`
|
|
72
|
+
(`@wirecat/cli-core/services`) and `mayDecide` (`@wirecat/cli-core/store`).** The registry lists every pending
|
|
73
|
+
proposal newest first across kinds (`relationship`, `key_document`, `fact`, `memory`, `findings` for evidence
|
|
74
|
+
findings, and `action` for every other stored proposal), and decides many ids at once, each on its own: the answer
|
|
75
|
+
lists `decided` and `refused`, and one refusal never undoes the others. A new kind registers itself with its id
|
|
76
|
+
`prefix` or the `stored` proposal kinds it owns; nothing else changes. `parseProposalIds` reads
|
|
77
|
+
`17070-17079,fact:500-502,17080/f2` and refuses a range across prefixes. Unconfirmed agent and rule links show as
|
|
78
|
+
`link:<id>` (relationships) and `key:<id>` (key documents) until the store moves them.
|
|
79
|
+
- **`knowledge.proposeRelation(scope, input, by)`**: an agent's or a rule's relationship is a stored `relationship`
|
|
80
|
+
proposal for the owner, not a link; the same pending pair returns the proposal already held. Removing a link to a
|
|
81
|
+
project, or rejecting any suggested link, leaves a rejected `relationship` proposal, so a later scan can skip it.
|
|
82
|
+
- **`proposedActions.list({ kinds, except, project, proposedBy })`**, `proposedBy` on `facts.list` and
|
|
83
|
+
`memories.list`, `order: "created"` on `facts.list`, and the bot's `kind` (`agent` or `rule`) on the author of a
|
|
84
|
+
proposed action, a fact, a memory and an evidence findings batch; bots may have the kind `rule`, and an `Author` may
|
|
85
|
+
name it.
|
|
86
|
+
- **`knowledge.decideLinkProposal(id, decision)`** decides a stored `relationship` or `key_document` proposal, and
|
|
87
|
+
**`knowledge.decideSuggestedLink(id, decision)`** an older unconfirmed link, each in one transaction: the link and
|
|
88
|
+
the decision land together or not at all. A decision's `reason` is kept: on a rejected proposal as its result, on a
|
|
89
|
+
fact in `metadata.decision`, on an evidence findings receipt.
|
|
90
|
+
|
|
91
|
+
### Changed — may break callers
|
|
92
|
+
|
|
93
|
+
- **`SERVER_BOUNDS.maxMessageBytes`, `ServerSearched.tooLarge` and `search.server-too-large` are gone**: a big
|
|
94
|
+
candidate is no longer left on the server; see `textBytes`, `ServerSearched.partial` and `search.server-partial`.
|
|
95
|
+
- **An option after `--` is read as the option.** `messages sync -- -100 --json` now syncs `-100` and answers in JSON;
|
|
96
|
+
it failed with "too many arguments". After `--`, a word that is exactly one of the command's options, its own or a
|
|
97
|
+
global one (`--json`, `--limit 5`, `--json=id`), moves in front of `--`; every other word stays a positional. A
|
|
98
|
+
negative id needs no `--` at all. Commands that take free text keep every word after `--` literal: `messages send` and
|
|
99
|
+
`edit` (also the bot's), `polls create`, `topics create`, `chats create`, `chats folders create`, `contacts rename`,
|
|
100
|
+
`contacts aliases set`, `messages press`, `bot commands set`, `schedule command`, `events remind`, `complete`, and the
|
|
101
|
+
notes templates. Mark such a command with `annotate(command, { literal_after_dashes: true })`; MCP calls are never
|
|
102
|
+
lifted. A script that passed the literal word `--json` as a positional of another command now gets the option instead.
|
|
103
|
+
- **A write to a calendar the account may only read refuses before anything is asked or journaled**
|
|
104
|
+
(`calendar.read-only`): create, quick add, import, update, delete, respond and move, for a feed and for a provider's
|
|
105
|
+
`reader` or `free-busy` calendar alike. Before, only the target of a move was checked; the provider refused the rest.
|
|
106
|
+
`calendars busy` and `search --backend server` leave feed calendars out.
|
|
107
|
+
- **A locator of another account or messenger is refused with a new text and a reason.** `messages show`, `context`,
|
|
108
|
+
`link` and `download` said "that locator belongs to another account; select its profile first" or "…belongs to
|
|
109
|
+
another messenger"; they now say `<ref> belongs to account <id>; profile <name> is account <id>` (`ref.wrong-account`)
|
|
110
|
+
or `<ref> is a <messenger> message; open it with …` (`ref.wrong-provider`). A script matching the old text matches
|
|
111
|
+
the reason instead.
|
|
112
|
+
- **A text of the form `<kind>:<no spaces>` given as a chat is read as a reference**, so a chat titled `note:plan` is
|
|
113
|
+
no longer found by that title; give its id or a part of the title.
|
|
114
|
+
- The provider seeds of `telegram`, `max` and `whatsapp` name their CLI (`command: tg`, `max`, `wa`), which a
|
|
115
|
+
provider clash message now shows (`telegram (messages, from tg)`).
|
|
116
|
+
- **Every decide method takes who decides, a `Decider`** (`"owner"` or `{ bot, model?, reason }`):
|
|
117
|
+
`facts.accept(ref, by)`, `facts.reject(ref, by)`, `memories.confirm(ref, by)`, `knowledge.confirmRelation(scope, id,
|
|
118
|
+
by)`, `knowledge.removeRelation(scope, id, by)`, `proposedActions.approve(id, by, result?)`,
|
|
119
|
+
`proposedActions.reject(id, by)`, `approveAsTask(store, id, by)`, and `by` is now required in
|
|
120
|
+
`evidenceFindings.review` and `meetingFindings.review`. `DocumentKnowledge.fact` and `.memory` take `by` for a
|
|
121
|
+
`confirm`, and `memories.markStale(ref, by)` and `.memory(…, "stale", by)` take it too: marking an agent's memory
|
|
122
|
+
stale is how it is turned down. `knowledge.relate` needs `by`: the owner's relationship is a link, an agent's is a
|
|
123
|
+
`relationship` proposal it returns instead. `notes.addLink` refuses any confirmed link without `by: "owner"`, so an
|
|
124
|
+
agent writes `confirmed: false` (the task service now does). `notes.removeLink(id, by)`, `facts.reverse(ref, by)`,
|
|
125
|
+
`facts.remove(ref, by)` and `memories.remove(ref, by)` take who removes; the document port's `addMemory` needs
|
|
126
|
+
`author`, `addFact` and `addLink` need `by`, with no owner default. `proposalRegistry(store)` takes no policy: the
|
|
127
|
+
host sets it once with `openStore({ decisions })`, and `store.decisionPolicy` is what the registry and every store
|
|
128
|
+
check read. A host passes the owner only when the owner runs the command, and
|
|
129
|
+
the agent's bot and reason otherwise.
|
|
130
|
+
An agent's confirmation of a relationship is recorded in the link's provenance, and of a fact or proposal as its
|
|
131
|
+
decider.
|
|
132
|
+
|
|
133
|
+
### Fixed
|
|
134
|
+
|
|
135
|
+
- **A sum written in groups of three read as the wrong amount: `50 000 ₽` was `0` and `4 300 ₽` was `300`**, because
|
|
136
|
+
only the last group was read. Groups after a space, a no-break or thin space, a dot or a comma (`50 000`,
|
|
137
|
+
`1.200 €`, `$1,200`) are one number now, a decimal part after them is kept (`5 000.50 ₽`), and amounts keep two
|
|
138
|
+
decimals instead of a whole number (`15.5 eur` was 16). This is the amount the duplicate check compares between
|
|
139
|
+
two tasks, so a reminder worded with a grouped sum is no longer compared on the wrong figure.
|
|
140
|
+
- **`page.next` of `messages list` and `messages comments` runs as written.** It repeated the query's `id`/`date`
|
|
141
|
+
bound beside `--cursor`, which the command refuses as two starting points; it now keeps only `topic:` and
|
|
142
|
+
`starred:`.
|
|
143
|
+
- **An inclusive start keeps a message sent exactly at it.** `messages list 'date>=2026-10-01'` skipped a message
|
|
144
|
+
sent at the first millisecond of the day, and `date<="…"` one sent at that moment.
|
|
145
|
+
|
|
146
|
+
### Security
|
|
147
|
+
|
|
148
|
+
- **An agent can no longer decide its own proposal.** Before, `facts.accept`/`reject`,
|
|
149
|
+
`memories.confirm`, `knowledge.confirmRelation`, `proposedActions.approve`/`reject`, `approveAsTask` and both
|
|
150
|
+
findings reviews never asked who decided, so over MCP an agent could confirm what it had proposed itself. Every one
|
|
151
|
+
of them, and the registry, now runs `mayDecide`: the owner always may; an agent needs a reason
|
|
152
|
+
(`proposal.reason-required`), is refused its own bot's proposal (`proposal.own`), a rule scorer's proposal and one
|
|
153
|
+
that names no author (`proposal.owner-only`), and every decision when the host turns agent decisions off
|
|
154
|
+
(`proposals.agent-decisions-off`). An agent can no longer add a memory already `confirmed`, mark its own memory
|
|
155
|
+
stale, write a confirmed project link itself, or remove a link. A link records only free-text provenance, which
|
|
156
|
+
anyone can write, so only the owner decides an unconfirmed link or removes a link; the rejected record a removal
|
|
157
|
+
leaves names who removed it. Findings an agent accepted for the owner record that agent, not the owner. Only the
|
|
158
|
+
owner removes or reverses an accepted fact or another bot's memory; an agent withdraws only its own row while it is
|
|
159
|
+
proposed. Turning agent decisions off now holds for every store method, not only the registry, and a bot is the
|
|
160
|
+
same bot whatever its letter case (`Claude-Code` is `claude-code`).
|
|
161
|
+
|
|
10
162
|
## 0.546.0 — 02.11.2026
|
|
11
163
|
|
|
12
164
|
### Added
|
|
@@ -9,14 +9,20 @@ export declare const historyQuery: (messenger: {
|
|
|
9
9
|
}) => import("@wirecat/cli-base/query").QueryRegistry;
|
|
10
10
|
declare const COMMENTS_QUERY: import("@wirecat/cli-base/query").QueryRegistry;
|
|
11
11
|
export { COMMENTS_QUERY };
|
|
12
|
-
type Start = Pick<ListWindow, "before" | "beforeTime" | "after">;
|
|
12
|
+
type Start = Pick<ListWindow, "before" | "beforeTime" | "after" | "until">;
|
|
13
13
|
/**
|
|
14
|
-
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward.
|
|
15
|
-
*
|
|
14
|
+
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. A window's end
|
|
15
|
+
* travels as `~<epoch ms>`, so a resumed read stops where the first one would have. Ids are opaque, so only the first
|
|
16
|
+
* colon splits it.
|
|
16
17
|
*/
|
|
17
18
|
export declare const cursorOf: (start: Start, edge: {
|
|
18
19
|
first?: string;
|
|
19
20
|
last?: string;
|
|
20
21
|
}) => string | undefined;
|
|
21
|
-
/** A list reads one way from one place: the query or the cursor names at most one
|
|
22
|
+
/** A list reads one way from one place: the query or the cursor names at most one window. */
|
|
22
23
|
export declare const historyWindow: (words: QueryWords, cursor: string | undefined, command: string, registry?: import("@wirecat/cli-base/query").QueryRegistry) => Omit<ListWindow, "limit">;
|
|
24
|
+
/**
|
|
25
|
+
* The query `page.next` reruns with its cursor: the topic and the stars, never the bounds, which the cursor carries
|
|
26
|
+
* and which would name a second starting point beside it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const resumeQuery: (words: QueryWords, command: string, registry?: import("@wirecat/cli-base/query").QueryRegistry) => string | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import {
|
|
3
|
-
import { listQuery, nativeField,
|
|
2
|
+
import { dateRange, defineQuery, queryError, timezoneOf } from "@wirecat/cli-base/query";
|
|
3
|
+
import { listQuery, nativeField, nativeTermLists, typedQuery } from "../list-query.js";
|
|
4
4
|
/** What a chat's history can be read by: where it starts, and one topic. */
|
|
5
5
|
export const HISTORY_QUERY = defineQuery({
|
|
6
6
|
entity: "message",
|
|
@@ -36,52 +36,108 @@ const COMMENTS_QUERY = defineQuery({
|
|
|
36
36
|
});
|
|
37
37
|
export { COMMENTS_QUERY };
|
|
38
38
|
/**
|
|
39
|
-
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward.
|
|
40
|
-
*
|
|
39
|
+
* Where the last page stopped and which way it read: `before:<id>` goes on back, `after:<id>` forward. A window's end
|
|
40
|
+
* travels as `~<epoch ms>`, so a resumed read stops where the first one would have. Ids are opaque, so only the first
|
|
41
|
+
* colon splits it.
|
|
41
42
|
*/
|
|
42
43
|
export const cursorOf = (start, edge) => {
|
|
43
44
|
const forward = start.after !== undefined;
|
|
44
45
|
const id = forward ? edge.last : edge.first;
|
|
45
|
-
|
|
46
|
+
if (id === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const end = forward && start.until !== undefined ? `~${start.until}` : "";
|
|
49
|
+
return `${forward ? "after" : "before"}:${id}${end}`;
|
|
46
50
|
};
|
|
47
51
|
const isId = (value) => value !== "" && !/[\s\p{Cc}]/u.test(value);
|
|
48
52
|
const fromCursor = (cursor) => {
|
|
49
53
|
const at = cursor.indexOf(":");
|
|
50
|
-
const [way,
|
|
54
|
+
const [way, rest] = at < 0 ? [cursor, ""] : [cursor.slice(0, at), cursor.slice(at + 1)];
|
|
55
|
+
const ended = way === "after" ? /^(.*)~(\d+)$/u.exec(rest) : null;
|
|
56
|
+
const id = ended ? ended[1] : rest;
|
|
51
57
|
if (!isId(id) || (way !== "before" && way !== "after"))
|
|
52
58
|
throw new CliError("validation_error", `--cursor takes what page.next gave, before:<id> or after:<id> — not "${cursor}"`);
|
|
53
|
-
|
|
59
|
+
if (way === "before")
|
|
60
|
+
return { before: id };
|
|
61
|
+
return { after: { id }, ...(ended ? { until: Number(ended[2]) } : {}) };
|
|
54
62
|
};
|
|
55
|
-
|
|
63
|
+
/** A term's lower and upper bound; a date's as moments in the zone, an exclusive end 1 ms inside. */
|
|
64
|
+
const boundsOf = (term, command) => {
|
|
56
65
|
const context = { command };
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const hasLower = term.value !== "*";
|
|
67
|
+
const hasUpper = term.upper !== undefined && term.upper !== "*";
|
|
68
|
+
const span = term.span;
|
|
69
|
+
if (term.field === "id") {
|
|
70
|
+
if (hasLower === hasUpper)
|
|
71
|
+
queryError("query.unsupported-operator", span, "id takes one side: id< or id>", context);
|
|
72
|
+
return hasLower
|
|
73
|
+
? { lower: { start: { after: { id: term.value } }, span, date: false } }
|
|
74
|
+
: { upper: { start: { before: term.upper }, span, date: false } };
|
|
75
|
+
}
|
|
76
|
+
if (!hasLower && !hasUpper)
|
|
77
|
+
queryError("query.unsupported-operator", span, `${term.field} needs a start, an end or both`, context);
|
|
78
|
+
const range = dateRange(term.value, term.upper ?? "*", term.lower_inclusive ?? true, term.upper_inclusive ?? true, timezoneOf(), span);
|
|
79
|
+
if ((hasLower && range.lower === undefined) || (hasUpper && range.upper === undefined))
|
|
80
|
+
queryError("query.invalid-date", span, "that is not a time", context);
|
|
81
|
+
return {
|
|
82
|
+
// The messenger and the store read strictly after a start, so an inclusive one moves 1 ms back.
|
|
83
|
+
...(range.lower === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { lower: { start: { after: { time: range.lower - (range.lower_inclusive ? 1 : 0) } }, span, date: true } }),
|
|
86
|
+
...(range.upper === undefined
|
|
87
|
+
? {}
|
|
88
|
+
: { upper: { start: { until: range.upper - (range.upper_inclusive ? 0 : 1) }, span, date: true } }),
|
|
89
|
+
};
|
|
69
90
|
};
|
|
70
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The window the query names. A start reads forward from it, oldest first, and stops at an end; an end alone reads
|
|
93
|
+
* back from it, newest page first.
|
|
94
|
+
*/
|
|
95
|
+
const windowOf = (terms, command) => {
|
|
96
|
+
const context = { command };
|
|
97
|
+
const lowers = [];
|
|
98
|
+
const uppers = [];
|
|
99
|
+
for (const term of terms) {
|
|
100
|
+
const { lower, upper } = boundsOf(term, command);
|
|
101
|
+
if (lower)
|
|
102
|
+
lowers.push(lower);
|
|
103
|
+
if (upper)
|
|
104
|
+
uppers.push(upper);
|
|
105
|
+
}
|
|
106
|
+
const [lower, extraLower] = lowers;
|
|
107
|
+
const [upper, extraUpper] = uppers;
|
|
108
|
+
if (extraLower)
|
|
109
|
+
queryError("query.unsupported-placement", extraLower.span, "the window has one start", context);
|
|
110
|
+
if (extraUpper)
|
|
111
|
+
queryError("query.unsupported-placement", extraUpper.span, "the window has one end", context);
|
|
112
|
+
if (lower && upper && !upper.date)
|
|
113
|
+
queryError("query.unsupported-placement", upper.span, "a window from a start ends at a date: date<…", context);
|
|
114
|
+
if (lower)
|
|
115
|
+
return { ...lower.start, ...upper?.start };
|
|
116
|
+
if (upper?.start.until !== undefined)
|
|
117
|
+
return { beforeTime: upper.start.until + 1 };
|
|
118
|
+
return upper?.start ?? {};
|
|
119
|
+
};
|
|
120
|
+
/** A list reads one way from one place: the query or the cursor names at most one window. */
|
|
71
121
|
export const historyWindow = (words, cursor, command, registry = HISTORY_QUERY) => {
|
|
72
|
-
const terms =
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (cursor !== undefined)
|
|
77
|
-
starts.push(fromCursor(cursor));
|
|
78
|
-
if (starts.length > 1)
|
|
122
|
+
const terms = nativeTermLists(listQuery(typedQuery(words), registry, command), command, ["date", "id"]);
|
|
123
|
+
const bounds = [...(terms.get("date") ?? []), ...(terms.get("id") ?? [])];
|
|
124
|
+
const start = windowOf(bounds, command);
|
|
125
|
+
if (cursor !== undefined && bounds.length > 0)
|
|
79
126
|
throw new CliError("validation_error", "the query and --cursor name two starting points; give one of them");
|
|
80
|
-
const topic = terms.
|
|
81
|
-
const starred = terms.
|
|
127
|
+
const topic = terms.get("topic")?.[0]?.value;
|
|
128
|
+
const starred = terms.get("starred")?.[0]?.value.toLowerCase();
|
|
82
129
|
return {
|
|
83
|
-
...
|
|
130
|
+
...(cursor === undefined ? start : fromCursor(cursor)),
|
|
84
131
|
...(topic === undefined ? {} : { threadId: topic }),
|
|
85
132
|
...(starred === undefined ? {} : { starred: starred === "true" }),
|
|
86
133
|
};
|
|
87
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* The query `page.next` reruns with its cursor: the topic and the stars, never the bounds, which the cursor carries
|
|
137
|
+
* and which would name a second starting point beside it.
|
|
138
|
+
*/
|
|
139
|
+
export const resumeQuery = (words, command, registry = HISTORY_QUERY) => {
|
|
140
|
+
const terms = nativeTermLists(listQuery(typedQuery(words), registry, command), command, ["date", "id"]);
|
|
141
|
+
const kept = ["topic", "starred"].flatMap((field) => terms.get(field)?.map((term) => `${field}:${term.value}`) ?? []);
|
|
142
|
+
return kept.length > 0 ? kept.join(" ") : undefined;
|
|
143
|
+
};
|
|
@@ -12,6 +12,7 @@ import { modelWith, spoken } from "../../speech/hearing.js";
|
|
|
12
12
|
import { REMOTE_READ } from "../effects.js";
|
|
13
13
|
import { itemOf, listOf, renderAnswer } from "../envelope.js";
|
|
14
14
|
import { queryHelp } from "../list-query.js";
|
|
15
|
+
import { withOptionHints } from "../option-hints.js";
|
|
15
16
|
import { positiveCount } from "../paging.js";
|
|
16
17
|
import { syncCommand } from "./backfill-command.js";
|
|
17
18
|
import { messengerContext } from "./context.js";
|
|
@@ -19,7 +20,7 @@ import { linksCommand } from "./conversations-command.js";
|
|
|
19
20
|
import { downloadSubcommand } from "./download-command.js";
|
|
20
21
|
import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
|
|
21
22
|
import { historyLimitGaps } from "./history-gaps.js";
|
|
22
|
-
import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
|
|
23
|
+
import { cursorOf, historyQuery, historyWindow, resumeQuery } from "./history-query.js";
|
|
23
24
|
import { hideNever } from "./lacks.js";
|
|
24
25
|
import { threadAnswer } from "./message-answers.js";
|
|
25
26
|
import { messageItem, messageItems, viewOf } from "./message-items.js";
|
|
@@ -36,6 +37,8 @@ import { sendCommand } from "./messages-send-command.js";
|
|
|
36
37
|
import { coverageGaps } from "./search-answers.js";
|
|
37
38
|
import { threadOptions, threadRequest } from "./thread-options.js";
|
|
38
39
|
import { transcribeSubcommand } from "./transcribe-command.js";
|
|
40
|
+
const WINDOW_EXAMPLE = "date>=2026-10-01 date<2026-10-20";
|
|
41
|
+
const TIME_FLAGS = ["--since", "--since-time", "--until", "--until-time", "--after-time", "--before-time"];
|
|
39
42
|
/** `messages`: reading, and sending through the guard. A CLI may add its own subcommands. */
|
|
40
43
|
export const messagesCommand = (messenger) => {
|
|
41
44
|
const messages = new Command("messages").description("read and send messages");
|
|
@@ -51,8 +54,8 @@ export const messagesCommand = (messenger) => {
|
|
|
51
54
|
.addHelpText("after", queryHelp(history))
|
|
52
55
|
.argument("<chat>", messenger.chatArgument)
|
|
53
56
|
.argument("[query...]", `${messenger.topics === false
|
|
54
|
-
?
|
|
55
|
-
:
|
|
57
|
+
? `where to start or which window: id<42, ${WINDOW_EXAMPLE}`
|
|
58
|
+
: `where to start or which window, and which topic: id<42, ${WINDOW_EXAMPLE}, topic:12`}${messenger.messageStars === true ? "; starred:true keeps the ones you starred" : ""}`)
|
|
56
59
|
.option("--limit <n>", "how many", positiveCount("--limit"))
|
|
57
60
|
.option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it")
|
|
58
61
|
.option(...TRANSCRIBE_OPTION)
|
|
@@ -73,6 +76,8 @@ export const messagesCommand = (messenger) => {
|
|
|
73
76
|
const next = page.hasMore
|
|
74
77
|
? cursorOf(window, { first: page.items[0]?.id, last: page.items.at(-1)?.id })
|
|
75
78
|
: undefined;
|
|
79
|
+
const typed = next === undefined ? query.join(" ") : resumeQuery(query, "messages list", history);
|
|
80
|
+
const queryArg = typed ? { query: typed } : {};
|
|
76
81
|
// Offline, the store is the whole answer: say how much of the chat it holds. Online the messenger is read.
|
|
77
82
|
const gaps = context.settings.offline
|
|
78
83
|
? await context.withStore(async (store, account) => {
|
|
@@ -88,13 +93,14 @@ export const messagesCommand = (messenger) => {
|
|
|
88
93
|
has_more: page.hasMore,
|
|
89
94
|
paging: "cursor",
|
|
90
95
|
...(next === undefined ? {} : { cursor: next }),
|
|
91
|
-
args: { chat, ...
|
|
96
|
+
args: { chat, ...queryArg },
|
|
92
97
|
next: gaps.next,
|
|
93
98
|
missing: [...hearingMissing(hearing, transcribe === true), ...gaps.missing],
|
|
94
99
|
...(gaps.missing.length > 0 ? { meta: { complete: false } } : {}),
|
|
95
100
|
}), () => renderMessages(spokenItems(page.items, hearing), viewOf(context, messenger)));
|
|
96
101
|
});
|
|
97
102
|
annotate(list, { output: { entity: "message" }, query: history });
|
|
103
|
+
withOptionHints(list, Object.fromEntries(TIME_FLAGS.map((flag) => [flag, `messages list reads a window by its query: '${WINDOW_EXAMPLE}'`])));
|
|
98
104
|
const readWindow = async (command, chat, message, window) => {
|
|
99
105
|
const context = messengerContext(command, messenger);
|
|
100
106
|
const { found, reader } = await context.withServices(async (services) => ({
|
|
@@ -7,7 +7,7 @@ import { listOf, renderAnswer } from "../envelope.js";
|
|
|
7
7
|
import { queryHelp } from "../list-query.js";
|
|
8
8
|
import { positiveCount } from "../paging.js";
|
|
9
9
|
import { messengerContext } from "./context.js";
|
|
10
|
-
import { COMMENTS_QUERY, cursorOf, historyWindow } from "./history-query.js";
|
|
10
|
+
import { COMMENTS_QUERY, cursorOf, historyWindow, resumeQuery } from "./history-query.js";
|
|
11
11
|
import { messageItems, viewOf } from "./message-items.js";
|
|
12
12
|
export const commentsCommand = (messenger) => {
|
|
13
13
|
const otherPosts = messenger.readsComments === true && messenger.channelComments === false;
|
|
@@ -42,13 +42,15 @@ export const commentsCommand = (messenger) => {
|
|
|
42
42
|
reader: await services.messages.reader(),
|
|
43
43
|
}));
|
|
44
44
|
const next = found.hasMore ? cursorOf(window, { first: found.items[0]?.id }) : undefined;
|
|
45
|
+
const typed = next === undefined ? query.join(" ") : resumeQuery(query, "messages comments", COMMENTS_QUERY);
|
|
46
|
+
const queryArg = typed ? { query: typed } : {};
|
|
45
47
|
renderAnswer(this, context, listOf("messages comments", {
|
|
46
48
|
items: messageItems(found.items, reader),
|
|
47
49
|
limit,
|
|
48
50
|
has_more: found.hasMore,
|
|
49
51
|
paging: "cursor",
|
|
50
52
|
...(next === undefined ? {} : { cursor: next }),
|
|
51
|
-
args: { chat, post, ...
|
|
53
|
+
args: { chat, post, ...queryArg },
|
|
52
54
|
}), () => renderMessages(found.items, viewOf(context, messenger)));
|
|
53
55
|
});
|
|
54
56
|
return annotate(command, { output: { entity: "message" }, query: COMMENTS_QUERY });
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
+
error?: undefined;
|
|
6
7
|
path: string;
|
|
7
8
|
exists: boolean;
|
|
8
|
-
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* A flag a command does not take, but an agent is likely to type, fails with what to type instead rather than
|
|
4
|
+
* commander's bare "unknown option".
|
|
5
|
+
*/
|
|
6
|
+
export declare const withOptionHints: (command: Command, hints: Readonly<Record<string, string>>) => Command;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A flag a command does not take, but an agent is likely to type, fails with what to type instead rather than
|
|
3
|
+
* commander's bare "unknown option".
|
|
4
|
+
*/
|
|
5
|
+
export const withOptionHints = (command, hints) => {
|
|
6
|
+
const unknown = command;
|
|
7
|
+
const fallback = unknown.unknownOption.bind(command);
|
|
8
|
+
unknown.unknownOption = (flag) => {
|
|
9
|
+
const hint = hints[flag.split("=")[0]];
|
|
10
|
+
if (hint === undefined)
|
|
11
|
+
return fallback(flag);
|
|
12
|
+
return command.error(`unknown option '${flag}' — ${hint}`, { code: "commander.unknownOption" });
|
|
13
|
+
};
|
|
14
|
+
return command;
|
|
15
|
+
};
|
package/dist/errors/reasons.js
CHANGED
|
@@ -110,7 +110,11 @@ const OTHERS = [
|
|
|
110
110
|
["search.server-failed", "provider_unavailable", "the server did not answer a search"],
|
|
111
111
|
["search.server-timeout", "timeout", "the server did not answer within --server-time"],
|
|
112
112
|
["search.server-capped", "outcome_unknown", "the server found more than one search reads, or a branch was not sent"],
|
|
113
|
-
[
|
|
113
|
+
[
|
|
114
|
+
"search.server-partial",
|
|
115
|
+
"outcome_unknown",
|
|
116
|
+
"a message the server found was stored without its attachments; the next sync reads it whole",
|
|
117
|
+
],
|
|
114
118
|
validation("store.refused", "the local store refused the document text"),
|
|
115
119
|
["store.not-writable", "permission_error", "the message store cannot be written from this process"],
|
|
116
120
|
[
|
|
@@ -145,6 +149,14 @@ const OTHERS = [
|
|
|
145
149
|
validation("name.taken", "the name is already used"),
|
|
146
150
|
validation("period.ambiguous", "the period can be read more than one way"),
|
|
147
151
|
validation("proposal.wrong-state", "the proposal is not in a state that allows this"),
|
|
152
|
+
validation("proposal.reason-required", "an agent deciding for the owner gives a reason"),
|
|
153
|
+
validation("proposal.decider-required", "a decision names who decides: the owner or an agent with a reason"),
|
|
154
|
+
validation("proposal.ids", "the proposal ids cannot be read"),
|
|
155
|
+
["proposal.own", "permission_error", "an agent never decides its own proposal"],
|
|
156
|
+
["proposal.owner-only", "permission_error", "only the owner decides this proposal"],
|
|
157
|
+
["proposals.agent-decisions-off", "permission_error", "agent decisions are turned off; the owner decides"],
|
|
158
|
+
["proposal.unknown-kind", "not_found", "no registered proposal kind holds this id"],
|
|
159
|
+
validation("relationship.exists", "a confirmed relationship already links them"),
|
|
148
160
|
validation("ranking.invalid", "the ranking is not valid"),
|
|
149
161
|
validation("ref.malformed", "the reference has a known kind but not its shape"),
|
|
150
162
|
validation("ref.unknown-kind", "the text names no kind of reference"),
|
package/dist/mail/cli/index.js
CHANGED
|
@@ -74,11 +74,11 @@ const serverIssues = (server) => {
|
|
|
74
74
|
...FAILED[reason],
|
|
75
75
|
source: "server",
|
|
76
76
|
}));
|
|
77
|
-
const
|
|
78
|
-
? [{ code: "outcome_unknown", reason: "search.server-
|
|
77
|
+
const partial = server.partial
|
|
78
|
+
? [{ code: "outcome_unknown", reason: "search.server-partial", source: "server" }]
|
|
79
79
|
: [];
|
|
80
|
-
return failed.length ||
|
|
81
|
-
? [...failed, ...
|
|
80
|
+
return failed.length || partial.length
|
|
81
|
+
? [...failed, ...partial]
|
|
82
82
|
: [{ code: "outcome_unknown", reason: "search.server-capped", source: "server" }];
|
|
83
83
|
};
|
|
84
84
|
const searchCommand = (mail) => {
|
package/dist/mail/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { type AuthVerdicts, HEADER_FIELDS, headersOf, type MailHeaders, mailHead
|
|
|
7
7
|
export { type MailIdsInput, mailIds } from "./ids.js";
|
|
8
8
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
9
9
|
export { type Address, htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, type ParsedMail, parseMail, } from "./parse.js";
|
|
10
|
+
export { PARTIAL, type PartialOutcome } from "./partial.js";
|
|
10
11
|
export { PRESET_NAMES, PRESETS, type PresetName, presetMatches } from "./presets.js";
|
|
11
12
|
export { domainsToAge, groupKey, type Probe, type ProbeGroup, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
12
13
|
export { type MailFolderProgress, mailProgress } from "./progress.js";
|
|
@@ -21,5 +22,6 @@ export { DEMOTE_AT, type DomainAges, demotedSenders, type LookupResult, SCORE_VE
|
|
|
21
22
|
export { type MailCriteria, mailCriteriaOf, narrows } from "./server-query.js";
|
|
22
23
|
export { type MailServerAccess, type MailServerRequest, searchMailServer } from "./server-search.js";
|
|
23
24
|
export type { Listed, MailSource, Quick, Searched } from "./source.js";
|
|
25
|
+
export { SEARCH_REFUSED, searchRefused } from "./source.js";
|
|
24
26
|
export { listedOf, type StoredMail } from "./stored.js";
|
|
25
27
|
export { MAX_ATTACHMENT_CHARS, type MailAccount, type MailIndexed, type MailSyncOptions, type MailSyncResult, RETRYABLE_ATTACHMENTS, syncMail, } from "./sync.js";
|
package/dist/mail/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { HEADER_FIELDS, headersOf, mailHeadersOf } from "./headers.js";
|
|
|
7
7
|
export { mailIds } from "./ids.js";
|
|
8
8
|
export { decodedLabelName, decodeMailboxName, labelName, systemLabel } from "./labels.js";
|
|
9
9
|
export { htmlToText, looksLikeHtml, MAX_ATTACHMENTS, MAX_BODY_CHARS, parseMail, } from "./parse.js";
|
|
10
|
+
export { PARTIAL } from "./partial.js";
|
|
10
11
|
export { PRESET_NAMES, PRESETS, presetMatches } from "./presets.js";
|
|
11
12
|
export { domainsToAge, groupKey, probeMail, SUGGEST_RULE } from "./probe.js";
|
|
12
13
|
export { mailProgress } from "./progress.js";
|
|
@@ -20,5 +21,6 @@ export { FRAUD_SIGNALS, FREE_MAIL, registrableOf, SCORE_RULE, scoreGroup, WEIGHT
|
|
|
20
21
|
export { DEMOTE_AT, demotedSenders, SCORE_VERSION, scoreSenders, } from "./sender-scores.js";
|
|
21
22
|
export { mailCriteriaOf, narrows } from "./server-query.js";
|
|
22
23
|
export { searchMailServer } from "./server-search.js";
|
|
24
|
+
export { SEARCH_REFUSED, searchRefused } from "./source.js";
|
|
23
25
|
export { listedOf } from "./stored.js";
|
|
24
26
|
export { MAX_ATTACHMENT_CHARS, RETRYABLE_ATTACHMENTS, syncMail, } from "./sync.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BudgetStop } from "../background/budget.js";
|
|
2
|
+
import type { LoadEngine } from "../readers/types.js";
|
|
3
|
+
import type { MessageStore } from "../store/index.js";
|
|
4
|
+
import type { MailSource } from "./source.js";
|
|
5
|
+
/** The `emails.metadata` key of an email stored from its first bytes: `{ bytes }` read of its `size`. */
|
|
6
|
+
export declare const PARTIAL = "partial";
|
|
7
|
+
/** How many partial emails one sync reads whole at most; the rest wait for the next run. */
|
|
8
|
+
export declare const PARTIAL_PER_RUN = 100;
|
|
9
|
+
export interface PartialOutcome {
|
|
10
|
+
/** Read whole this run, the mark cleared. */
|
|
11
|
+
completed: number;
|
|
12
|
+
/** Still partial after this run. */
|
|
13
|
+
left: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads whole the emails a server search stored from their first bytes, so the archive holds their attachments too.
|
|
17
|
+
* Sync never reads stored mail again, so without this step a partial email would stay partial. Gmail's are found by
|
|
18
|
+
* their id; elsewhere at a stored location, trusted only when the bytes read there carry the stored id.
|
|
19
|
+
*/
|
|
20
|
+
export declare const completePartial: ({ store, source, accountId, address, folders, maxBodyChars, maxAttachmentChars, loadEngine, now, take, signal, }: {
|
|
21
|
+
store: MessageStore;
|
|
22
|
+
source: MailSource;
|
|
23
|
+
accountId: number;
|
|
24
|
+
address: string;
|
|
25
|
+
/** Where Gmail's are looked for by id; asked only when a partial email is there to read. */
|
|
26
|
+
folders: () => Promise<readonly string[]>;
|
|
27
|
+
maxBodyChars: number;
|
|
28
|
+
maxAttachmentChars: number;
|
|
29
|
+
loadEngine: LoadEngine;
|
|
30
|
+
now: number;
|
|
31
|
+
/** A budgeted run's next request, or why there is none. */
|
|
32
|
+
take?: () => BudgetStop | null;
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}) => Promise<PartialOutcome>;
|