@wirecat/cli-core 0.381.0 → 0.383.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 +50 -0
- package/dist/attachments/documents.d.ts +3 -0
- package/dist/attachments/documents.js +17 -0
- package/dist/cli/bot/watch.js +3 -0
- package/dist/cli/messenger/chat-answers.d.ts +2 -0
- package/dist/cli/messenger/chat-answers.js +2 -0
- package/dist/cli/messenger/chat-lists.d.ts +2 -0
- package/dist/cli/messenger/context.d.ts +2 -2
- package/dist/cli/messenger/context.js +2 -1
- package/dist/cli/messenger/conversation-answers.d.ts +1 -0
- package/dist/cli/messenger/message-answers.d.ts +1 -0
- package/dist/cli/messenger/message-items.js +1 -0
- package/dist/cli/messenger/message-writes.d.ts +1 -0
- package/dist/cli/messenger/serve-relay.d.ts +1 -0
- package/dist/cli/messenger/stored.d.ts +3 -1
- package/dist/cli/messenger/stored.js +12 -5
- package/dist/cli/messenger/watch-command.js +2 -0
- package/dist/documents/blocks.d.ts +45 -0
- package/dist/documents/blocks.js +48 -0
- package/dist/documents/index.d.ts +5 -0
- package/dist/documents/index.js +4 -0
- package/dist/documents/markdown-blocks.d.ts +3 -0
- package/dist/documents/markdown-blocks.js +123 -0
- package/dist/documents/push-model.d.ts +89 -0
- package/dist/documents/push-model.js +1 -0
- package/dist/documents.d.ts +1 -0
- package/dist/documents.js +1 -0
- package/dist/domain/models.d.ts +31 -4
- package/dist/entities/chat.d.ts +4 -0
- package/dist/entities/chat.js +4 -0
- package/dist/entities/contact.d.ts +4 -0
- package/dist/entities/evidence.d.ts +8 -0
- package/dist/entities/index.d.ts +17 -0
- package/dist/entities/message.d.ts +2 -0
- package/dist/entities/message.js +2 -0
- package/dist/errors/reasons.js +3 -0
- package/dist/meetings/answers.d.ts +1 -0
- package/dist/meetings/index.d.ts +1 -1
- package/dist/meetings/index.js +1 -1
- package/dist/meetings/item.d.ts +10 -0
- package/dist/meetings/item.js +34 -18
- package/dist/ports/traits.d.ts +6 -0
- package/dist/services/chats.js +16 -2
- package/dist/services/deps.d.ts +2 -0
- package/dist/services/deps.js +2 -0
- package/dist/services/documents/block-diff.d.ts +24 -0
- package/dist/services/documents/block-diff.js +91 -0
- package/dist/services/documents/push.d.ts +60 -0
- package/dist/services/documents/push.js +218 -0
- package/dist/services/inbox.d.ts +1 -0
- package/dist/services/inbox.js +10 -3
- package/dist/store/keys.d.ts +1 -1
- package/dist/store/messages.d.ts +7 -1
- package/dist/store/messages.js +16 -3
- package/dist/store/sqlite/chat-rekey.js +9 -1
- package/dist/store/sqlite/chats.d.ts +2 -2
- package/dist/store/sqlite/chats.js +37 -8
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/meeting-hit-query.js +18 -3
- package/dist/store/sqlite/messages.d.ts +9 -2
- package/dist/store/sqlite/messages.js +29 -2
- package/dist/store/sqlite/migrations.generated.js +16 -0
- package/dist/store/sqlite/reads.d.ts +50 -0
- package/dist/store/sqlite/reads.js +3 -1
- package/dist/store/sqlite/schema.d.ts +96 -0
- package/dist/store/sqlite/schema.js +6 -0
- package/dist/store/sqlite/search.d.ts +33 -0
- package/dist/store/sqlite/values.d.ts +4 -0
- package/dist/store/sqlite/values.js +3 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,47 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.383.0 — 22.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`readSpreadsheet(bytes, "xlsx" | "ods")` from `@wirecat/cli-core/documents`** returns a workbook's cells per
|
|
15
|
+
worksheet (values as displayed), the same reader text extraction uses, so a provider can mirror a workbook's tabs.
|
|
16
|
+
- **Push local files into a provider as native documents (`@wirecat/cli-core/documents`).** `pushDocuments` mirrors
|
|
17
|
+
a local folder one way through a provider's `DocumentTarget`: new files are created, changed files updated, and
|
|
18
|
+
unchanged files skipped by content hash with no remote call. Before a create it saves a pending origin key, so a
|
|
19
|
+
crash after the provider wrote finds the file (`findByOrigin`) instead of creating a duplicate. The provider's
|
|
20
|
+
`prepare` compares the remote with the last push before writing; a remote copy changed since then is a conflict
|
|
21
|
+
unless `force`, and only the `write` step passes the guard. A local delete is reported as an
|
|
22
|
+
orphan and nothing is ever deleted remotely. `planPush` is the pure plan behind `dryRun`; `scanLocal` skips hidden
|
|
23
|
+
files, editor leftovers and symbolic links. Every write passes through the caller's `guard` with its own operation
|
|
24
|
+
id. For documents with structure, `markdownBlocks` (async, its parser loaded on first use) parses Markdown (CommonMark and GFM tables, task lists,
|
|
25
|
+
strikethrough) into `Block`s a provider writes natively, `diffBlocks` gives the edits that leave unchanged blocks
|
|
26
|
+
in place, and `reconcileBlocks` lets a checklist tick change on either side without a conflict. See
|
|
27
|
+
[documents](docs/dev/DOCUMENTS.md#push-local-files-to-a-provider).
|
|
28
|
+
|
|
29
|
+
## 0.382.0 — 22.10.2026
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **A chat's state from the phone (store version 28).** The store keeps `Chat.muted`, `Chat.archived` and two new
|
|
34
|
+
fields: `mutedUntil` (when a mute ends, UTC; none is for good) and `markedUnread` (the owner marked the chat unread).
|
|
35
|
+
A `ChatUpdate` that leaves one out keeps the stored value, and a mute past its end reads as over. Stored chats show
|
|
36
|
+
them, and `chat` gains `muted_until` and `marked_unread`. So `inbox` and `review` over the store now leave out muted
|
|
37
|
+
and archived chats, as they do for a messenger that answers live, and `inbox` takes in a chat marked unread with its
|
|
38
|
+
newest message from someone else. tg and max send `muted` and `archived` in their chat lists, so their stored chats
|
|
39
|
+
now show them too.
|
|
40
|
+
- **The store can count unread messages** for a messenger that sets `unreadCounts: "store"`: each new message from
|
|
41
|
+
someone else that `watch` or `listen` saves after the messenger's last count adds one, and a whole-chat `chats read`
|
|
42
|
+
stores 0. `store.messages.saveMessages` takes `countUnread`. tg and max leave it unset and count as before. See
|
|
43
|
+
[adapters](docs/dev/ADAPTERS.md).
|
|
44
|
+
- **Delivered and read on the owner's own messages (store version 27).** `Message.delivery` (`delivered` or `read`)
|
|
45
|
+
says how far the owner's message in a one-to-one chat got; a `watch` event `{ event: "delivery", chatId, chatTitle,
|
|
46
|
+
messageId, delivery, at }` (`MessageDeliveryEvent`) carries a receipt as it comes. `store.messages.saveDelivery` and
|
|
47
|
+
a saved message's own `delivery` keep it in the new `messages.delivery` column, only on the owner's messages and
|
|
48
|
+
only ever forward. Stored messages show it as `delivery`. tg and max send none and see no change. See
|
|
49
|
+
[adapters](docs/dev/ADAPTERS.md).
|
|
50
|
+
|
|
10
51
|
## 0.381.0 — 22.10.2026
|
|
11
52
|
|
|
12
53
|
### Added
|
|
@@ -14,6 +55,10 @@ shape of this file.
|
|
|
14
55
|
- **Meeting search finds words speech recognition cut off.** A stored word of four letters or more that begins a
|
|
15
56
|
query word, and that the Russian dictionary does not know, widens it: «може» for «может», «напри» for «например».
|
|
16
57
|
Real shorter words («после» for «последующих») still do not. This is ztx's rule, behind the dictionary.
|
|
58
|
+
- **A meeting answer names what about it is doubtful, in `issues`.** `MEETING_ISSUES` lists the values; today one,
|
|
59
|
+
`time_without_zone`: the provider sent a time with no zone, and it was read in the computer's zone. A provider
|
|
60
|
+
adapter keeps the data and puts the issue in the meeting's `metadata.issues`; `meetingItem` shows the known ones and
|
|
61
|
+
leaves the field out when there are none. No store change.
|
|
17
62
|
|
|
18
63
|
- **People's reactions in the store (store version 26).** For a messenger that sends one person's reaction change at a
|
|
19
64
|
time (WhatsApp), a `watch` event `{ event: "person_reaction", chatId, chatTitle, messageId, reaction }`
|
|
@@ -30,6 +75,11 @@ shape of this file.
|
|
|
30
75
|
(U+FE0F, U+FE0E, or the U+200D joiner) that a typed one lacks, so "Семья❤️" did not find the chat "Семья❤". Names
|
|
31
76
|
now compare without those marks and with accented letters in one form, still ignoring case, wherever a chat or a
|
|
32
77
|
person is named (`pickChat`, `pickPerson`, a stored alias).
|
|
78
|
+
- **`from:` finds a speaker known only by name across meetings.** A participant keyed by name in one meeting
|
|
79
|
+
(`name:…@account/occurrence:n`, as zoom-cli's `.txt` speakers and nameless API participants are) is grouped with
|
|
80
|
+
the same name elsewhere, and with the one identified speaker of that name when there is exactly one. Before,
|
|
81
|
+
each meeting's copy was a separate candidate, so `from:"Name"` was ambiguous. Candidates with an email show it.
|
|
82
|
+
|
|
33
83
|
- **A store the process may not write is a permission error, not a crash.** Opening the store for writing where the
|
|
34
84
|
file or its folder is read-only (`EROFS`, `EACCES`, `EPERM`) was reported as `internal_error` "this is a bug".
|
|
35
85
|
It is now `permission_error` (exit 5) with reason `store.not-writable`, and the message names no path.
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Extraction, FileHint } from "../readers/types.js";
|
|
2
|
+
import { type Sheet } from "./sheets.js";
|
|
2
3
|
export type DocumentKind = "odt" | "ods" | "odp" | "xlsx" | "pptx" | "epub";
|
|
3
4
|
export declare const documentKind: (hint: FileHint) => DocumentKind | undefined;
|
|
4
5
|
/** v2 for spreadsheets: rows under their header instead of one line per cell, so older text is read again. */
|
|
5
6
|
export declare const documentExtractor: (kind: DocumentKind) => string;
|
|
7
|
+
/** The cells of an XLSX or ODS workbook, one `Sheet` per worksheet in workbook order, values as displayed. */
|
|
8
|
+
export declare const readSpreadsheet: (bytes: Uint8Array, kind: "xlsx" | "ods", signal?: AbortSignal) => Promise<Sheet[]>;
|
|
6
9
|
export declare const readDocument: (bytes: Uint8Array, kind: DocumentKind, signal?: AbortSignal) => Promise<Extraction>;
|
|
@@ -393,6 +393,23 @@ const readBook = (archive, output) => {
|
|
|
393
393
|
visit(bodies[0]);
|
|
394
394
|
}
|
|
395
395
|
};
|
|
396
|
+
/** The cells of an XLSX or ODS workbook, one `Sheet` per worksheet in workbook order, values as displayed. */
|
|
397
|
+
export const readSpreadsheet = async (bytes, kind, signal) => {
|
|
398
|
+
try {
|
|
399
|
+
const archive = await readContainer(bytes, signal);
|
|
400
|
+
const sheets = kind === "xlsx" ? readSheet(archive) : readOdf(archive, kind, new Output());
|
|
401
|
+
if (!sheets)
|
|
402
|
+
throw new Error("wrong_format");
|
|
403
|
+
return sheets;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (error instanceof CliError)
|
|
407
|
+
throw error;
|
|
408
|
+
throw new CliError("validation_error", `the ${kind.toUpperCase()} workbook could not be read`, {
|
|
409
|
+
reason: "spreadsheet.unreadable",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
};
|
|
396
413
|
export const readDocument = async (bytes, kind, signal) => {
|
|
397
414
|
const extractor = documentExtractor(kind);
|
|
398
415
|
try {
|
package/dist/cli/bot/watch.js
CHANGED
|
@@ -140,6 +140,9 @@ export const botWatchCommand = (bot) => annotate(new Command("watch"), { effects
|
|
|
140
140
|
else if (event.event === "star") {
|
|
141
141
|
context.streams.data(`${event.starred ? "starred" : "unstarred"} in ${title(event.chatId)}: message ${event.messageId}\n`);
|
|
142
142
|
}
|
|
143
|
+
else if (event.event === "delivery") {
|
|
144
|
+
context.streams.data(`${event.delivery} in ${title(event.chatId)}: message ${event.messageId}\n`);
|
|
145
|
+
}
|
|
143
146
|
else if (event.event === "chat_event") {
|
|
144
147
|
context.streams.data(`${event.change.event} in ${title(event.chatId)}\n`);
|
|
145
148
|
}
|
|
@@ -61,7 +61,9 @@ export declare const chatShown: (account: AccountKey, card: Parameters<typeof ch
|
|
|
61
61
|
last_message_at: string | null;
|
|
62
62
|
participants_count: number | null;
|
|
63
63
|
muted?: boolean | undefined;
|
|
64
|
+
muted_until?: string | undefined;
|
|
64
65
|
archived?: boolean | undefined;
|
|
66
|
+
marked_unread?: boolean | undefined;
|
|
65
67
|
membership_state?: "archived" | "external" | "imported" | "joined" | "left" | "public" | undefined;
|
|
66
68
|
parent_chat_id?: string | undefined;
|
|
67
69
|
scope?: "personal" | "work" | undefined;
|
|
@@ -28,7 +28,9 @@ export const chatAnswer = (account, chat) => defined({
|
|
|
28
28
|
last_message_at: chat.lastMessageAt,
|
|
29
29
|
participants_count: chat.participantsCount,
|
|
30
30
|
muted: chat.muted,
|
|
31
|
+
muted_until: chat.mutedUntil ?? undefined,
|
|
31
32
|
archived: chat.archived,
|
|
33
|
+
marked_unread: chat.markedUnread,
|
|
32
34
|
membership_state: chat.membershipState,
|
|
33
35
|
parent_chat_id: chat.parentChatId ?? undefined,
|
|
34
36
|
scope: chat.scope,
|
|
@@ -21,7 +21,9 @@ export declare const chatsList: (services: Services, query: QueryWords, paging:
|
|
|
21
21
|
last_message_at: string | null;
|
|
22
22
|
participants_count: number | null;
|
|
23
23
|
muted?: boolean | undefined;
|
|
24
|
+
muted_until?: string | undefined;
|
|
24
25
|
archived?: boolean | undefined;
|
|
26
|
+
marked_unread?: boolean | undefined;
|
|
25
27
|
membership_state?: "archived" | "external" | "imported" | "joined" | "left" | "public" | undefined;
|
|
26
28
|
parent_chat_id?: string | undefined;
|
|
27
29
|
scope?: "personal" | "work" | undefined;
|
|
@@ -236,12 +236,12 @@ type ConnectionWaits = Pick<BaseEnvironment, "closeWaitMs" | "rekeyPacing">;
|
|
|
236
236
|
* does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
|
|
237
237
|
* A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
|
|
238
238
|
*/
|
|
239
|
-
export declare const connected: (connection: MessengerAdapter, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }: Pick<Messenger, "app" | "provider" | "name" | "deletedWithoutChat" | "pace" | "history" | "savedChatId">, { settings, env, renderer }: Pick<BaseContext, "settings" | "env" | "renderer">, events: EventSink, store?: LazyStore, { listen: watching, closeWaitMs, rekeyPacing, }?: Pick<ConnectOptions, "listen"> & ConnectionWaits) => {
|
|
239
|
+
export declare const connected: (connection: MessengerAdapter, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, unreadCounts, }: Pick<Messenger, "app" | "provider" | "name" | "deletedWithoutChat" | "pace" | "history" | "savedChatId" | "unreadCounts">, { settings, env, renderer }: Pick<BaseContext, "settings" | "env" | "renderer">, events: EventSink, store?: LazyStore, { listen: watching, closeWaitMs, rekeyPacing, }?: Pick<ConnectOptions, "listen"> & ConnectionWaits) => {
|
|
240
240
|
adapter: MessengerAdapter;
|
|
241
241
|
close: () => Promise<void>;
|
|
242
242
|
};
|
|
243
243
|
/** `connectFor` and then `connected`, over one store handle: what the reads save and what the adapter looks up. */
|
|
244
|
-
export declare const connectedFor: (command: Command, messenger: Pick<Messenger, "connect" | "app" | "provider" | "name" | "deletedWithoutChat" | "pace" | "history" | "savedChatId">, base: BaseContext, options: ConnectOptions & {
|
|
244
|
+
export declare const connectedFor: (command: Command, messenger: Pick<Messenger, "connect" | "app" | "provider" | "name" | "deletedWithoutChat" | "pace" | "history" | "savedChatId" | "unreadCounts">, base: BaseContext, options: ConnectOptions & {
|
|
245
245
|
events: EventSink;
|
|
246
246
|
}) => Promise<{
|
|
247
247
|
connection: MessengerAdapter;
|
|
@@ -189,7 +189,7 @@ const waitsOf = (command) => {
|
|
|
189
189
|
* does not `watch` is open: what it queued while nothing was connected is delivered once and cannot be fetched again.
|
|
190
190
|
* A connection opened with `listen: true` is left to its `watch` and `feed`, which save the same.
|
|
191
191
|
*/
|
|
192
|
-
export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, }, { settings, env, renderer }, events, store = lazyStore(env, app.command), { listen: watching = false, closeWaitMs = SAVES_WAIT_MS, rekeyPacing, } = {}) => {
|
|
192
|
+
export const connected = (connection, { app, provider, name, deletedWithoutChat, pace, history, savedChatId, unreadCounts, }, { settings, env, renderer }, events, store = lazyStore(env, app.command), { listen: watching = false, closeWaitMs = SAVES_WAIT_MS, rekeyPacing, } = {}) => {
|
|
193
193
|
const self = connection.self();
|
|
194
194
|
if (self !== null)
|
|
195
195
|
rememberAccount(app, settings.profile, self, env);
|
|
@@ -220,6 +220,7 @@ export const connected = (connection, { app, provider, name, deletedWithoutChat,
|
|
|
220
220
|
rekeyed: (from, to) => rekeyRecipients(app, settings.profile, env, renderer.warn, from, to),
|
|
221
221
|
recipientIds: () => (new RecipientList(recipientsPathFor(app, settings.profile, env), app.command).read() ?? []).flatMap((recipient) => [recipient.id, ...(recipient.partnerId ? [recipient.partnerId] : [])]),
|
|
222
222
|
...(history === "store" ? { namesFromStore: savedChatId ? { savedChatId } : {} } : {}),
|
|
223
|
+
...(unreadCounts === "store" ? { countsUnread: true } : {}),
|
|
223
224
|
});
|
|
224
225
|
const passives = history === "store" && self !== null && !watching
|
|
225
226
|
? passivesOf(saving).map((passive) => listened(passive, events, renderer.warn, closeWaitMs))
|
|
@@ -175,6 +175,7 @@ export declare const conversationShown: (account: AccountKey, { summary, message
|
|
|
175
175
|
} | null | undefined;
|
|
176
176
|
scheduled_for?: string | undefined;
|
|
177
177
|
starred?: true | undefined;
|
|
178
|
+
delivery?: "delivered" | "read" | undefined;
|
|
178
179
|
email?: {
|
|
179
180
|
subject: string | null;
|
|
180
181
|
from: {
|
|
@@ -152,6 +152,7 @@ export declare const threadAnswer: ({ items, ...thread }: ThreadContext, reader:
|
|
|
152
152
|
} | null | undefined;
|
|
153
153
|
scheduled_for?: string | undefined;
|
|
154
154
|
starred?: true | undefined;
|
|
155
|
+
delivery?: "delivered" | "read" | undefined;
|
|
155
156
|
email?: {
|
|
156
157
|
subject: string | null;
|
|
157
158
|
from: {
|
|
@@ -59,6 +59,7 @@ export const messageItem = (message, reader) => {
|
|
|
59
59
|
reactions: message.reactions,
|
|
60
60
|
scheduled_for: message.scheduledFor,
|
|
61
61
|
starred: message.starred === true ? true : undefined,
|
|
62
|
+
delivery: message.delivery,
|
|
62
63
|
email: message.email === undefined
|
|
63
64
|
? undefined
|
|
64
65
|
: {
|
|
@@ -154,6 +154,7 @@ export declare const writtenAnswer: ({ operationId, sendId, message, scheduledFo
|
|
|
154
154
|
} | null | undefined;
|
|
155
155
|
scheduled_for?: string | undefined;
|
|
156
156
|
starred?: true | undefined;
|
|
157
|
+
delivery?: "delivered" | "read" | undefined;
|
|
157
158
|
email?: {
|
|
158
159
|
subject: string | null;
|
|
159
160
|
from: {
|
|
@@ -32,6 +32,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
|
|
|
32
32
|
fetching?: import("../index.js").Fetching;
|
|
33
33
|
history?: "server" | "store";
|
|
34
34
|
historyOnDemand?: boolean;
|
|
35
|
+
unreadCounts?: "messenger" | "store";
|
|
35
36
|
serverSearch?: boolean | "chat";
|
|
36
37
|
services?: import("../../services/index.js").Override;
|
|
37
38
|
connection?: "exclusive";
|
|
@@ -39,6 +39,8 @@ export interface Saving {
|
|
|
39
39
|
* titles `watch` leaves out, are answered from the store first. Its server has no directory to ask.
|
|
40
40
|
*/
|
|
41
41
|
namesFromStore?: Pick<MessengerTraits, "savedChatId">;
|
|
42
|
+
/** `MessengerTraits.unreadCounts: "store"`: a message `watch` or `listen` saves raises its chat's unread count. */
|
|
43
|
+
countsUnread?: boolean;
|
|
42
44
|
}
|
|
43
45
|
/**
|
|
44
46
|
* The adapter, with what each read answers written to the store. **A failed write never fails the
|
|
@@ -48,4 +50,4 @@ export interface Saving {
|
|
|
48
50
|
* would erase what `chats list` stored. Nor are `chat` and `contact`: a card's member list is not
|
|
49
51
|
* known to be whole, and `saveMembers` replaces the list.
|
|
50
52
|
*/
|
|
51
|
-
export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, }: Saving) => MessengerAdapter;
|
|
53
|
+
export declare const stored: (messenger: MessengerAdapter, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing, closing, namesFromStore, recipientIds, countsUnread, }: Saving) => MessengerAdapter;
|
|
@@ -12,7 +12,7 @@ const REKEY_PACING = { lockMs: 50, pauseMs: 25 };
|
|
|
12
12
|
* would erase what `chats list` stored. Nor are `chat` and `contact`: a card's member list is not
|
|
13
13
|
* known to be whole, and `saveMembers` replaces the list.
|
|
14
14
|
*/
|
|
15
|
-
export const stored = (messenger, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing = REKEY_PACING, closing, namesFromStore, recipientIds, }) => {
|
|
15
|
+
export const stored = (messenger, { account, store, warn, events, pending, announcing, deletedWithoutChat, profile, rekeyed, rekeyPacing = REKEY_PACING, closing, namesFromStore, recipientIds, countsUnread = false, }) => {
|
|
16
16
|
let warned = false;
|
|
17
17
|
let recorded = profile === undefined;
|
|
18
18
|
const notWritten = (operation, error) => {
|
|
@@ -318,7 +318,7 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
318
318
|
await rememberTitles(store, account, titles);
|
|
319
319
|
return (messenger.watch?.((event) => {
|
|
320
320
|
const seenAt = Date.now();
|
|
321
|
-
void save("messages.watch", (opened) => keep(opened, account, event, seenAt, deletedWithoutChat));
|
|
321
|
+
void save("messages.watch", (opened) => keep(opened, account, event, seenAt, deletedWithoutChat, countsUnread));
|
|
322
322
|
onEvent(namesFromStore ? titled(event, titles) : event);
|
|
323
323
|
}, signal, onReady) ?? Promise.resolve());
|
|
324
324
|
},
|
|
@@ -327,7 +327,7 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
327
327
|
...(messenger.listen
|
|
328
328
|
? {
|
|
329
329
|
listen: (onEvent, signal) => messenger.listen?.((event) => {
|
|
330
|
-
void save("messages.listen", (opened) => keep(opened, account, event, Date.now(), deletedWithoutChat));
|
|
330
|
+
void save("messages.listen", (opened) => keep(opened, account, event, Date.now(), deletedWithoutChat, countsUnread));
|
|
331
331
|
onEvent(event);
|
|
332
332
|
}, signal) ?? Promise.resolve(),
|
|
333
333
|
}
|
|
@@ -541,12 +541,16 @@ const fill = async (store, account, batch) => {
|
|
|
541
541
|
* Each change as the store keeps it: a message or an edit upserted, a deletion a tombstone, reactions replaced, a
|
|
542
542
|
* voter's vote or a person's reaction replacing their older one, a star set or taken off, a chat event kept once.
|
|
543
543
|
*/
|
|
544
|
-
const keep = async (store, account, event, seenAt, among) => {
|
|
544
|
+
const keep = async (store, account, event, seenAt, among, countsUnread) => {
|
|
545
545
|
switch (event.event) {
|
|
546
546
|
case "message":
|
|
547
547
|
case "edit": {
|
|
548
548
|
const { chatTitle, ...message } = event.message;
|
|
549
|
-
await store.messages.saveMessages(account, message.chatId, [message], {
|
|
549
|
+
await store.messages.saveMessages(account, message.chatId, [message], {
|
|
550
|
+
via: "update",
|
|
551
|
+
seenAt,
|
|
552
|
+
...(countsUnread && event.event === "message" ? { countUnread: true } : {}),
|
|
553
|
+
});
|
|
550
554
|
return;
|
|
551
555
|
}
|
|
552
556
|
case "delete":
|
|
@@ -561,6 +565,9 @@ const keep = async (store, account, event, seenAt, among) => {
|
|
|
561
565
|
case "star":
|
|
562
566
|
await store.messages.saveStar(account, event.chatId, event.messageId, event.starred);
|
|
563
567
|
return;
|
|
568
|
+
case "delivery":
|
|
569
|
+
await store.messages.saveDelivery(account, event.chatId, event.messageId, event.delivery);
|
|
570
|
+
return;
|
|
564
571
|
case "chat_event":
|
|
565
572
|
await store.roster.saveChatEvents(account, event.chatId, [event.change]);
|
|
566
573
|
return;
|
|
@@ -118,6 +118,8 @@ const describe = (event, render) => {
|
|
|
118
118
|
}
|
|
119
119
|
case "star":
|
|
120
120
|
return `${event.starred ? "starred" : "unstarred"} in ${event.chatTitle ?? event.chatId}: message ${event.messageId}\n`;
|
|
121
|
+
case "delivery":
|
|
122
|
+
return `${event.delivery} in ${event.chatTitle ?? event.chatId}: message ${event.messageId}\n`;
|
|
121
123
|
case "chat_event": {
|
|
122
124
|
const { event: action, by, people } = event.change;
|
|
123
125
|
const who = people.map((one) => one.name ?? one.id).join(", ");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface Inline {
|
|
2
|
+
text: string;
|
|
3
|
+
bold?: true;
|
|
4
|
+
italic?: true;
|
|
5
|
+
strike?: true;
|
|
6
|
+
code?: true;
|
|
7
|
+
link?: string;
|
|
8
|
+
}
|
|
9
|
+
export type ListKind = "bullet" | "number" | "check";
|
|
10
|
+
export type Block = {
|
|
11
|
+
kind: "heading";
|
|
12
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
13
|
+
inlines: Inline[];
|
|
14
|
+
} | {
|
|
15
|
+
kind: "paragraph";
|
|
16
|
+
inlines: Inline[];
|
|
17
|
+
} | {
|
|
18
|
+
kind: "item";
|
|
19
|
+
list: ListKind;
|
|
20
|
+
depth: number;
|
|
21
|
+
checked?: boolean;
|
|
22
|
+
inlines: Inline[];
|
|
23
|
+
} | {
|
|
24
|
+
kind: "code";
|
|
25
|
+
language?: string;
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "quote";
|
|
29
|
+
inlines: Inline[];
|
|
30
|
+
} | {
|
|
31
|
+
kind: "table";
|
|
32
|
+
header: Inline[][];
|
|
33
|
+
rows: Inline[][][];
|
|
34
|
+
} | {
|
|
35
|
+
kind: "image";
|
|
36
|
+
url: string;
|
|
37
|
+
alt: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "rule";
|
|
40
|
+
};
|
|
41
|
+
/** Adjacent runs with the same marks merge and empty runs drop, so two equal texts give one key. */
|
|
42
|
+
export declare const normalizeInlines: (inlines: Inline[]) => Inline[];
|
|
43
|
+
export declare const blockKey: (block: Block) => string;
|
|
44
|
+
/** The same key with checklist ticks left out: two blocks that differ only in a tick share it. */
|
|
45
|
+
export declare const blockShape: (block: Block) => string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const MARKS = ["bold", "italic", "strike", "code", "link"];
|
|
2
|
+
const sameMarks = (a, b) => MARKS.every((mark) => a[mark] === b[mark]);
|
|
3
|
+
/** Adjacent runs with the same marks merge and empty runs drop, so two equal texts give one key. */
|
|
4
|
+
export const normalizeInlines = (inlines) => {
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const inline of inlines) {
|
|
7
|
+
if (!inline.text)
|
|
8
|
+
continue;
|
|
9
|
+
const last = out.at(-1);
|
|
10
|
+
if (last && sameMarks(last, inline))
|
|
11
|
+
last.text += inline.text;
|
|
12
|
+
else
|
|
13
|
+
out.push({ ...inline });
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
const inlineKey = (inline) => [inline.text, ...MARKS.map((mark) => inline[mark] ?? "")];
|
|
18
|
+
const key = (block, withChecked) => {
|
|
19
|
+
switch (block.kind) {
|
|
20
|
+
case "heading":
|
|
21
|
+
return [block.kind, block.level, normalizeInlines(block.inlines).map(inlineKey)];
|
|
22
|
+
case "paragraph":
|
|
23
|
+
case "quote":
|
|
24
|
+
return [block.kind, normalizeInlines(block.inlines).map(inlineKey)];
|
|
25
|
+
case "item":
|
|
26
|
+
return [
|
|
27
|
+
block.kind,
|
|
28
|
+
block.list,
|
|
29
|
+
block.depth,
|
|
30
|
+
withChecked && block.list === "check" ? Boolean(block.checked) : "",
|
|
31
|
+
normalizeInlines(block.inlines).map(inlineKey),
|
|
32
|
+
];
|
|
33
|
+
case "code":
|
|
34
|
+
return [block.kind, block.language ?? "", block.text];
|
|
35
|
+
case "table":
|
|
36
|
+
return [
|
|
37
|
+
block.kind,
|
|
38
|
+
[block.header, ...block.rows].map((row) => row.map((cell) => normalizeInlines(cell).map(inlineKey))),
|
|
39
|
+
];
|
|
40
|
+
case "image":
|
|
41
|
+
return [block.kind, block.url, block.alt];
|
|
42
|
+
case "rule":
|
|
43
|
+
return [block.kind];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export const blockKey = (block) => JSON.stringify(key(block, true));
|
|
47
|
+
/** The same key with checklist ticks left out: two blocks that differ only in a tick share it. */
|
|
48
|
+
export const blockShape = (block) => JSON.stringify(key(block, false));
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
export { type BlockHunk, diffBlocks, type Reconciled, reconcileBlocks } from "../services/documents/block-diff.js";
|
|
1
2
|
export { type Complete, extractMemories } from "../services/documents/extraction.js";
|
|
2
3
|
export { documentIngestion, type IngestionOptions } from "../services/documents/ingestion.js";
|
|
3
4
|
export { type DocumentKnowledge, documentKnowledge } from "../services/documents/knowledge.js";
|
|
4
5
|
export type { SyncOptions } from "../services/documents/ports.js";
|
|
5
6
|
export { extractDocumentMemories, type ImportedSource, type ProposalOptions } from "../services/documents/proposals.js";
|
|
7
|
+
export { type PushOptions, type PushResult, planPush, pushDocuments, scanLocal, } from "../services/documents/push.js";
|
|
6
8
|
export { type SyncResult, syncDocuments } from "../services/documents/sync.js";
|
|
9
|
+
export { type Block, blockKey, blockShape, type Inline, type ListKind, normalizeInlines } from "./blocks.js";
|
|
7
10
|
export { catalogFiles } from "./catalog.js";
|
|
11
|
+
export { markdownBlocks } from "./markdown-blocks.js";
|
|
8
12
|
export type { DocumentSource, IndexedSource, PictureWriter, SourceChange, SourceContent, SourceFile, SourceIndex, SourceSkip, SourceState, SourceStatePort, SourceWalk, SourceWork, TabText, } from "./model.js";
|
|
13
|
+
export type { DocumentTarget, LocalDocument, Prepared, PushEntry, Pushed, PushPlan, PushState, PushStatePort, } from "./push-model.js";
|
|
9
14
|
export { within } from "./scope.js";
|
package/dist/documents/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
export { diffBlocks, reconcileBlocks } from "../services/documents/block-diff.js";
|
|
1
2
|
export { extractMemories } from "../services/documents/extraction.js";
|
|
2
3
|
export { documentIngestion } from "../services/documents/ingestion.js";
|
|
3
4
|
export { documentKnowledge } from "../services/documents/knowledge.js";
|
|
4
5
|
export { extractDocumentMemories } from "../services/documents/proposals.js";
|
|
6
|
+
export { planPush, pushDocuments, scanLocal, } from "../services/documents/push.js";
|
|
5
7
|
export { syncDocuments } from "../services/documents/sync.js";
|
|
8
|
+
export { blockKey, blockShape, normalizeInlines } from "./blocks.js";
|
|
6
9
|
export { catalogFiles } from "./catalog.js";
|
|
10
|
+
export { markdownBlocks } from "./markdown-blocks.js";
|
|
7
11
|
export { within } from "./scope.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { normalizeInlines } from "./blocks.js";
|
|
2
|
+
// Loaded on first use: importing the documents entry point would otherwise cost every command about 50 ms.
|
|
3
|
+
let parser;
|
|
4
|
+
const loadParser = () => (parser ??= Promise.all([
|
|
5
|
+
import("mdast-util-from-markdown"),
|
|
6
|
+
import("mdast-util-gfm"),
|
|
7
|
+
import("micromark-extension-gfm"),
|
|
8
|
+
]).then(([{ fromMarkdown }, { gfmFromMarkdown }, { gfm }]) => (markdown) => fromMarkdown(markdown, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })));
|
|
9
|
+
/** Markdown (CommonMark + GFM tables, task lists and strikethrough) as the blocks a provider can write natively. */
|
|
10
|
+
export const markdownBlocks = async (markdown) => {
|
|
11
|
+
const tree = (await loadParser())(markdown);
|
|
12
|
+
const definitions = new Map();
|
|
13
|
+
const collect = (node) => {
|
|
14
|
+
if (node.type === "definition")
|
|
15
|
+
definitions.set(node.identifier, node);
|
|
16
|
+
if ("children" in node)
|
|
17
|
+
for (const child of node.children)
|
|
18
|
+
collect(child);
|
|
19
|
+
};
|
|
20
|
+
collect(tree);
|
|
21
|
+
const images = [];
|
|
22
|
+
const inlinesOf = (nodes, marks = {}) => nodes.flatMap((node) => {
|
|
23
|
+
switch (node.type) {
|
|
24
|
+
case "text":
|
|
25
|
+
return [{ ...marks, text: node.value.replace(/\n/g, " ") }];
|
|
26
|
+
case "inlineCode":
|
|
27
|
+
return [{ ...marks, code: true, text: node.value }];
|
|
28
|
+
case "strong":
|
|
29
|
+
return inlinesOf(node.children, { ...marks, bold: true });
|
|
30
|
+
case "emphasis":
|
|
31
|
+
return inlinesOf(node.children, { ...marks, italic: true });
|
|
32
|
+
case "delete":
|
|
33
|
+
return inlinesOf(node.children, { ...marks, strike: true });
|
|
34
|
+
case "link":
|
|
35
|
+
return inlinesOf(node.children, { ...marks, link: node.url });
|
|
36
|
+
case "linkReference": {
|
|
37
|
+
const url = definitions.get(node.identifier)?.url;
|
|
38
|
+
return inlinesOf(node.children, url ? { ...marks, link: url } : marks);
|
|
39
|
+
}
|
|
40
|
+
case "break":
|
|
41
|
+
return [{ ...marks, text: "\n" }];
|
|
42
|
+
case "image":
|
|
43
|
+
images.push({ kind: "image", url: node.url, alt: node.alt ?? "" });
|
|
44
|
+
return [];
|
|
45
|
+
case "imageReference": {
|
|
46
|
+
const url = definitions.get(node.identifier)?.url;
|
|
47
|
+
if (url)
|
|
48
|
+
images.push({ kind: "image", url, alt: node.alt ?? "" });
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
case "html":
|
|
52
|
+
return [{ ...marks, text: node.value }];
|
|
53
|
+
default:
|
|
54
|
+
return "children" in node ? inlinesOf(node.children, marks) : [];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const trimmed = (inlines) => {
|
|
58
|
+
const out = normalizeInlines(inlines);
|
|
59
|
+
const first = out[0];
|
|
60
|
+
const last = out.at(-1);
|
|
61
|
+
if (first)
|
|
62
|
+
first.text = first.text.trimStart();
|
|
63
|
+
if (last)
|
|
64
|
+
last.text = last.text.trimEnd();
|
|
65
|
+
return normalizeInlines(out);
|
|
66
|
+
};
|
|
67
|
+
const withImages = (block) => {
|
|
68
|
+
const found = images.splice(0);
|
|
69
|
+
if (block?.kind === "paragraph" && !block.inlines.length)
|
|
70
|
+
return found;
|
|
71
|
+
return block ? [block, ...found] : found;
|
|
72
|
+
};
|
|
73
|
+
const textOf = (node) => node.children.flatMap((child) => child.type === "paragraph" || child.type === "heading" ? inlinesOf(child.children) : []);
|
|
74
|
+
const blocks = (nodes, depth = 0) => nodes.flatMap((node) => {
|
|
75
|
+
switch (node.type) {
|
|
76
|
+
case "heading":
|
|
77
|
+
return withImages({ kind: "heading", level: node.depth, inlines: trimmed(inlinesOf(node.children)) });
|
|
78
|
+
case "paragraph":
|
|
79
|
+
return withImages({ kind: "paragraph", inlines: trimmed(inlinesOf(node.children)) });
|
|
80
|
+
case "code":
|
|
81
|
+
return [{ kind: "code", ...(node.lang ? { language: node.lang } : {}), text: node.value }];
|
|
82
|
+
case "blockquote":
|
|
83
|
+
return node.children.flatMap((child) => child.type === "paragraph" || child.type === "heading"
|
|
84
|
+
? withImages({ kind: "quote", inlines: trimmed(inlinesOf(child.children)) })
|
|
85
|
+
: blocks([child], depth));
|
|
86
|
+
case "thematicBreak":
|
|
87
|
+
return [{ kind: "rule" }];
|
|
88
|
+
case "table": {
|
|
89
|
+
const [head, ...rows] = node.children.map((row) => row.children.map((cell) => trimmed(inlinesOf(cell.children))));
|
|
90
|
+
return [{ kind: "table", header: head ?? [], rows }];
|
|
91
|
+
}
|
|
92
|
+
case "list":
|
|
93
|
+
return node.children.flatMap((item) => {
|
|
94
|
+
const [first, ...rest] = item.children;
|
|
95
|
+
const head = {
|
|
96
|
+
kind: "item",
|
|
97
|
+
list: (item.checked != null ? "check" : node.ordered ? "number" : "bullet"),
|
|
98
|
+
depth,
|
|
99
|
+
...(item.checked != null ? { checked: item.checked } : {}),
|
|
100
|
+
inlines: trimmed(first && first.type === "paragraph" ? inlinesOf(first.children) : []),
|
|
101
|
+
};
|
|
102
|
+
const tail = first && first.type !== "paragraph" ? [first, ...rest] : rest;
|
|
103
|
+
return [
|
|
104
|
+
...withImages(head),
|
|
105
|
+
...tail.flatMap((child) => child.type === "list"
|
|
106
|
+
? blocks([child], depth + 1)
|
|
107
|
+
: child.type === "paragraph"
|
|
108
|
+
? withImages({ kind: "paragraph", inlines: trimmed(inlinesOf(child.children)) })
|
|
109
|
+
: blocks([child], depth)),
|
|
110
|
+
];
|
|
111
|
+
});
|
|
112
|
+
case "html":
|
|
113
|
+
return [{ kind: "paragraph", inlines: [{ text: node.value }] }];
|
|
114
|
+
case "definition":
|
|
115
|
+
case "footnoteDefinition":
|
|
116
|
+
case "yaml":
|
|
117
|
+
return [];
|
|
118
|
+
default:
|
|
119
|
+
return "children" in node ? withImages({ kind: "paragraph", inlines: trimmed(textOf(node)) }) : [];
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return blocks(tree.children);
|
|
123
|
+
};
|