@wirecat/cli-core 0.381.0 → 0.382.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 (53) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/cli/bot/watch.js +3 -0
  3. package/dist/cli/messenger/chat-answers.d.ts +2 -0
  4. package/dist/cli/messenger/chat-answers.js +2 -0
  5. package/dist/cli/messenger/chat-lists.d.ts +2 -0
  6. package/dist/cli/messenger/context.d.ts +2 -2
  7. package/dist/cli/messenger/context.js +2 -1
  8. package/dist/cli/messenger/conversation-answers.d.ts +1 -0
  9. package/dist/cli/messenger/message-answers.d.ts +1 -0
  10. package/dist/cli/messenger/message-items.js +1 -0
  11. package/dist/cli/messenger/message-writes.d.ts +1 -0
  12. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  13. package/dist/cli/messenger/stored.d.ts +3 -1
  14. package/dist/cli/messenger/stored.js +12 -5
  15. package/dist/cli/messenger/watch-command.js +2 -0
  16. package/dist/domain/models.d.ts +31 -4
  17. package/dist/entities/chat.d.ts +4 -0
  18. package/dist/entities/chat.js +4 -0
  19. package/dist/entities/contact.d.ts +4 -0
  20. package/dist/entities/evidence.d.ts +8 -0
  21. package/dist/entities/index.d.ts +17 -0
  22. package/dist/entities/message.d.ts +2 -0
  23. package/dist/entities/message.js +2 -0
  24. package/dist/meetings/answers.d.ts +1 -0
  25. package/dist/meetings/index.d.ts +1 -1
  26. package/dist/meetings/index.js +1 -1
  27. package/dist/meetings/item.d.ts +10 -0
  28. package/dist/meetings/item.js +34 -18
  29. package/dist/ports/traits.d.ts +6 -0
  30. package/dist/services/chats.js +16 -2
  31. package/dist/services/deps.d.ts +2 -0
  32. package/dist/services/deps.js +2 -0
  33. package/dist/services/inbox.d.ts +1 -0
  34. package/dist/services/inbox.js +10 -3
  35. package/dist/store/keys.d.ts +1 -1
  36. package/dist/store/messages.d.ts +7 -1
  37. package/dist/store/messages.js +16 -3
  38. package/dist/store/sqlite/chat-rekey.js +9 -1
  39. package/dist/store/sqlite/chats.d.ts +2 -2
  40. package/dist/store/sqlite/chats.js +37 -8
  41. package/dist/store/sqlite/manifest.js +2 -0
  42. package/dist/store/sqlite/meeting-hit-query.js +18 -3
  43. package/dist/store/sqlite/messages.d.ts +9 -2
  44. package/dist/store/sqlite/messages.js +29 -2
  45. package/dist/store/sqlite/migrations.generated.js +16 -0
  46. package/dist/store/sqlite/reads.d.ts +50 -0
  47. package/dist/store/sqlite/reads.js +3 -1
  48. package/dist/store/sqlite/schema.d.ts +96 -0
  49. package/dist/store/sqlite/schema.js +6 -0
  50. package/dist/store/sqlite/search.d.ts +33 -0
  51. package/dist/store/sqlite/values.d.ts +4 -0
  52. package/dist/store/sqlite/values.js +3 -0
  53. package/package.json +1 -1
@@ -7,7 +7,7 @@ import { folded } from "./folded.js";
7
7
  import { identityPk } from "./identities.js";
8
8
  import { attachments, chats, messageRevisions, messages, messageTranscripts } from "./schema.js";
9
9
  import { intoVoiceAttachment } from "./transcripts.js";
10
- import { json, parsed, toMs } from "./values.js";
10
+ import { DELIVERY, json, parsed, toMs } from "./values.js";
11
11
  import { purgeVectorsOf } from "./vectors.js";
12
12
  const FIELDS = [
13
13
  "threadExternalId",
@@ -120,7 +120,7 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
120
120
  const revived = found?.deletedAt != null && seenAt !== undefined && found.deletedAt < seenAt;
121
121
  // A deleted message leaves no text behind, and a sync that still carries it does not bring it back.
122
122
  if (found?.deletedAt != null && !revived)
123
- return;
123
+ return false;
124
124
  let pk;
125
125
  if (!found) {
126
126
  pk = Number(statements.insert.get({
@@ -189,6 +189,12 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
189
189
  .set({ starred: message.starred ? 1 : 0 })
190
190
  .where(eq(messages.id, pk))
191
191
  .run();
192
+ if (message.delivery !== undefined)
193
+ orm
194
+ .update(messages)
195
+ .set({ delivery: raised(message.delivery) })
196
+ .where(and(eq(messages.id, pk), eq(messages.outgoing, 1)))
197
+ .run();
192
198
  const root = message.threadRootId ?? message.threadId;
193
199
  if (root !== undefined) {
194
200
  context.database
@@ -222,6 +228,19 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
222
228
  });
223
229
  if (Number(heardFirst) > 0)
224
230
  intoVoiceAttachment(context, pk);
231
+ return !found;
232
+ };
233
+ /**
234
+ * One more unread for a message from someone else that the store first saw now, sent after the messenger's count was
235
+ * taken. A chat the messenger never gave a count for is left alone: the store would count its whole history.
236
+ */
237
+ export const countUnread = ({ database }, chatKey, message) => {
238
+ if (message.outgoing || message.editedAt !== null)
239
+ return;
240
+ database
241
+ .prepare(`UPDATE chats SET unread_count = unread_count + 1
242
+ WHERE id = ? AND unread_count IS NOT NULL AND unread_counted_at IS NOT NULL AND unread_counted_at < ?`)
243
+ .run(chatKey, Date.parse(message.timestamp));
225
244
  };
226
245
  /**
227
246
  * The owner's ruling (NEED-393 A): a deleted message keeps its row, so a later sync cannot bring it
@@ -268,6 +287,14 @@ export const saveStar = ({ orm }, chatKey, messageId, starred) => orm
268
287
  .where(and(eq(messages.chatId, chatKey), eq(messages.externalId, messageId), isNull(messages.deletedAt)))
269
288
  .returning({ pk: messages.id })
270
289
  .all().length > 0;
290
+ const raised = (delivery) => sql `max(coalesce(delivery, 0), ${DELIVERY[delivery]})`;
291
+ /** Answers whether the owner's own message is held. A receipt is for a message the owner sent, so another's keeps none. */
292
+ export const saveDelivery = ({ orm }, chatKey, messageId, delivery) => orm
293
+ .update(messages)
294
+ .set({ delivery: raised(delivery) })
295
+ .where(and(eq(messages.chatId, chatKey), eq(messages.externalId, messageId), eq(messages.outgoing, 1), isNull(messages.deletedAt)))
296
+ .returning({ pk: messages.id })
297
+ .all().length > 0;
271
298
  export const markDeleted = (context, accountKey, messageIds, chatId, among) => {
272
299
  const { orm } = context;
273
300
  const chatKey = chatId === undefined ? undefined : findChatPk(context, accountKey, chatId);
@@ -561,5 +561,21 @@ export const GENERATED = [
561
561
  "CREATE TABLE `message_reactions` (\n\t`id` integer PRIMARY KEY,\n\t`chat_id` integer NOT NULL,\n\t`message_external_id` text NOT NULL,\n\t`identity_id` integer NOT NULL,\n\t`own` integer NOT NULL,\n\t`reaction` text,\n\t`reacted_at` integer NOT NULL,\n\t`created_at` integer NOT NULL,\n\t`updated_at` integer NOT NULL,\n\tCONSTRAINT `fk_message_reactions_chat_id_chats_id_fk` FOREIGN KEY (`chat_id`) REFERENCES `chats`(`id`),\n\tCONSTRAINT `fk_message_reactions_identity_id_identities_id_fk` FOREIGN KEY (`identity_id`) REFERENCES `identities`(`id`),\n\tCONSTRAINT `message_reactions_chat_id_message_external_id_identity_id_unique` UNIQUE(`chat_id`,`message_external_id`,`identity_id`)\n);",
562
562
  "CREATE INDEX `message_reactions_by_identity_id` ON `message_reactions` (`identity_id`);"
563
563
  ]
564
+ },
565
+ {
566
+ "name": "20261021221808_version-27-message-delivery",
567
+ "statements": [
568
+ "ALTER TABLE `messages` ADD `delivery` integer;"
569
+ ]
570
+ },
571
+ {
572
+ "name": "20261021224625_version-28-chat-state",
573
+ "statements": [
574
+ "ALTER TABLE `chats` ADD `muted` integer;",
575
+ "ALTER TABLE `chats` ADD `muted_until` integer;",
576
+ "ALTER TABLE `chats` ADD `archived` integer;",
577
+ "ALTER TABLE `chats` ADD `marked_unread` integer;",
578
+ "ALTER TABLE `chats` ADD `unread_counted_at` integer;"
579
+ ]
564
580
  }
565
581
  ];
@@ -266,6 +266,22 @@ export declare const MESSAGE_FIELDS: {
266
266
  identity: undefined;
267
267
  generated: undefined;
268
268
  }, {}>;
269
+ delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
270
+ name: string;
271
+ tableName: "messages";
272
+ dataType: "number int53";
273
+ data: number;
274
+ driverParam: number;
275
+ notNull: false;
276
+ hasDefault: false;
277
+ isPrimaryKey: false;
278
+ isAutoincrement: false;
279
+ hasRuntimeDefault: false;
280
+ enumValues: undefined;
281
+ baseColumn: never;
282
+ identity: undefined;
283
+ generated: undefined;
284
+ }, {}>;
269
285
  };
270
286
  export interface MessageRow {
271
287
  pk: number;
@@ -286,6 +302,7 @@ export interface MessageRow {
286
302
  providerMetadata: string | null;
287
303
  mentions: string | null;
288
304
  starred: number | null;
305
+ delivery: number | null;
289
306
  }
290
307
  export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("drizzle-orm/sqlite-core").SQLiteAsyncSelectBase<"messages", "sync", unknown, {
291
308
  pk: import("drizzle-orm/sqlite-core").SQLiteColumn<{
@@ -546,6 +563,22 @@ export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("dri
546
563
  identity: undefined;
547
564
  generated: undefined;
548
565
  }, {}>;
566
+ delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
567
+ name: string;
568
+ tableName: "messages";
569
+ dataType: "number int53";
570
+ data: number;
571
+ driverParam: number;
572
+ notNull: false;
573
+ hasDefault: false;
574
+ isPrimaryKey: false;
575
+ isAutoincrement: false;
576
+ hasRuntimeDefault: false;
577
+ enumValues: undefined;
578
+ baseColumn: never;
579
+ identity: undefined;
580
+ generated: undefined;
581
+ }, {}>;
549
582
  }, "partial", Record<"messages", "not-null"> & {
550
583
  chats: "not-null";
551
584
  } & {
@@ -569,6 +602,7 @@ export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("dri
569
602
  providerMetadata: string | null;
570
603
  mentions: string | null;
571
604
  starred: number | null;
605
+ delivery: number | null;
572
606
  }[], {
573
607
  pk: import("drizzle-orm/sqlite-core").SQLiteColumn<{
574
608
  name: string;
@@ -828,6 +862,22 @@ export declare const selectMessages: ({ orm }: StoreContext) => Omit<import("dri
828
862
  identity: undefined;
829
863
  generated: undefined;
830
864
  }, {}>;
865
+ delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
866
+ name: string;
867
+ tableName: "messages";
868
+ dataType: "number int53";
869
+ data: number;
870
+ driverParam: number;
871
+ notNull: false;
872
+ hasDefault: false;
873
+ isPrimaryKey: false;
874
+ isAutoincrement: false;
875
+ hasRuntimeDefault: false;
876
+ enumValues: undefined;
877
+ baseColumn: never;
878
+ identity: undefined;
879
+ generated: undefined;
880
+ }, {}>;
831
881
  }>, never>;
832
882
  export declare const newestFirst: SQL<unknown>[];
833
883
  export declare const before: (sentAt: number, pk: number) => SQL<unknown>;
@@ -1,7 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { and, asc, desc, eq, gt, gte, inArray, isNull, lte, or, sql } from "./drizzle/core.js";
3
3
  import { attachments, chats, identities, messageRevisions, messages } from "./schema.js";
4
- import { parsed, present, toIso, toMs } from "./values.js";
4
+ import { deliveryOf, parsed, present, toIso, toMs } from "./values.js";
5
5
  /**
6
6
  * One flat row for every read, aliased where tables share a column name, so the same mapper serves
7
7
  * a plain select and a subquery (`find` per chat).
@@ -25,6 +25,7 @@ export const MESSAGE_FIELDS = {
25
25
  providerMetadata: messages.metadata,
26
26
  mentions: messages.mentions,
27
27
  starred: messages.starred,
28
+ delivery: messages.delivery,
28
29
  };
29
30
  export const selectMessages = ({ orm }) => orm
30
31
  .select(MESSAGE_FIELDS)
@@ -236,6 +237,7 @@ const toMessage = (row, attachments) => {
236
237
  reactions: parsed(row.reactions) ?? null,
237
238
  ...present({ providerMetadata: parsed(row.providerMetadata), mentions: parsed(row.mentions) }),
238
239
  ...(row.starred === 1 ? { starred: true } : {}),
240
+ ...present({ delivery: deliveryOf(row.delivery) }),
239
241
  };
240
242
  };
241
243
  /**
@@ -2596,6 +2596,86 @@ export declare const chats: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
2596
2596
  identity: undefined;
2597
2597
  generated: undefined;
2598
2598
  }, {}>;
2599
+ muted: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2600
+ name: string;
2601
+ tableName: "chats";
2602
+ dataType: "number int53";
2603
+ data: number;
2604
+ driverParam: number;
2605
+ notNull: false;
2606
+ hasDefault: false;
2607
+ isPrimaryKey: false;
2608
+ isAutoincrement: false;
2609
+ hasRuntimeDefault: false;
2610
+ enumValues: undefined;
2611
+ baseColumn: never;
2612
+ identity: undefined;
2613
+ generated: undefined;
2614
+ }, {}>;
2615
+ mutedUntil: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2616
+ name: string;
2617
+ tableName: "chats";
2618
+ dataType: "number int53";
2619
+ data: number;
2620
+ driverParam: number;
2621
+ notNull: false;
2622
+ hasDefault: false;
2623
+ isPrimaryKey: false;
2624
+ isAutoincrement: false;
2625
+ hasRuntimeDefault: false;
2626
+ enumValues: undefined;
2627
+ baseColumn: never;
2628
+ identity: undefined;
2629
+ generated: undefined;
2630
+ }, {}>;
2631
+ archived: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2632
+ name: string;
2633
+ tableName: "chats";
2634
+ dataType: "number int53";
2635
+ data: number;
2636
+ driverParam: number;
2637
+ notNull: false;
2638
+ hasDefault: false;
2639
+ isPrimaryKey: false;
2640
+ isAutoincrement: false;
2641
+ hasRuntimeDefault: false;
2642
+ enumValues: undefined;
2643
+ baseColumn: never;
2644
+ identity: undefined;
2645
+ generated: undefined;
2646
+ }, {}>;
2647
+ markedUnread: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2648
+ name: string;
2649
+ tableName: "chats";
2650
+ dataType: "number int53";
2651
+ data: number;
2652
+ driverParam: number;
2653
+ notNull: false;
2654
+ hasDefault: false;
2655
+ isPrimaryKey: false;
2656
+ isAutoincrement: false;
2657
+ hasRuntimeDefault: false;
2658
+ enumValues: undefined;
2659
+ baseColumn: never;
2660
+ identity: undefined;
2661
+ generated: undefined;
2662
+ }, {}>;
2663
+ unreadCountedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
2664
+ name: string;
2665
+ tableName: "chats";
2666
+ dataType: "number int53";
2667
+ data: number;
2668
+ driverParam: number;
2669
+ notNull: false;
2670
+ hasDefault: false;
2671
+ isPrimaryKey: false;
2672
+ isAutoincrement: false;
2673
+ hasRuntimeDefault: false;
2674
+ enumValues: undefined;
2675
+ baseColumn: never;
2676
+ identity: undefined;
2677
+ generated: undefined;
2678
+ }, {}>;
2599
2679
  };
2600
2680
  dialect: 'sqlite';
2601
2681
  }>;
@@ -3365,6 +3445,22 @@ export declare const messages: import("drizzle-orm/sqlite-core").SQLiteTableWith
3365
3445
  identity: undefined;
3366
3446
  generated: undefined;
3367
3447
  }, {}>;
3448
+ delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
3449
+ name: string;
3450
+ tableName: "messages";
3451
+ dataType: "number int53";
3452
+ data: number;
3453
+ driverParam: number;
3454
+ notNull: false;
3455
+ hasDefault: false;
3456
+ isPrimaryKey: false;
3457
+ isAutoincrement: false;
3458
+ hasRuntimeDefault: false;
3459
+ enumValues: undefined;
3460
+ baseColumn: never;
3461
+ identity: undefined;
3462
+ generated: undefined;
3463
+ }, {}>;
3368
3464
  };
3369
3465
  dialect: 'sqlite';
3370
3466
  }>;
@@ -261,6 +261,11 @@ export const chats = sqliteTable("chats", {
261
261
  createdAt: integer("created_at").notNull(),
262
262
  parentChatId: integer("parent_chat_id").references(() => chats.id),
263
263
  scope: text("scope"),
264
+ muted: integer("muted"),
265
+ mutedUntil: integer("muted_until"),
266
+ archived: integer("archived"),
267
+ markedUnread: integer("marked_unread"),
268
+ unreadCountedAt: integer("unread_counted_at"),
264
269
  }, (table) => [
265
270
  unique().on(table.accountId, table.externalId),
266
271
  index("chats_by_recency").on(table.accountId, desc(table.lastMessageAt)),
@@ -348,6 +353,7 @@ export const messages = sqliteTable("messages", {
348
353
  updatedAt: integer("updated_at").notNull(),
349
354
  threadRootId: integer("thread_root_id").references(() => messages.id),
350
355
  starred: integer("starred"),
356
+ delivery: integer("delivery"),
351
357
  }, (table) => [
352
358
  unique().on(table.chatId, table.externalId),
353
359
  index("messages_by_time").on(table.chatId, desc(table.sentAt)),
@@ -264,6 +264,22 @@ export declare const newestHits: (context: StoreContext, where: SQL | undefined,
264
264
  identity: undefined;
265
265
  generated: undefined;
266
266
  }, {}>;
267
+ delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
268
+ name: string;
269
+ tableName: "messages";
270
+ dataType: "number int53";
271
+ data: number;
272
+ driverParam: number;
273
+ notNull: false;
274
+ hasDefault: false;
275
+ isPrimaryKey: false;
276
+ isAutoincrement: false;
277
+ hasRuntimeDefault: false;
278
+ enumValues: undefined;
279
+ baseColumn: never;
280
+ identity: undefined;
281
+ generated: undefined;
282
+ }, {}>;
267
283
  chatTitle: SQL.Aliased<string | null>;
268
284
  provider: SQL.Aliased<string>;
269
285
  accountNativeId: SQL.Aliased<string>;
@@ -292,6 +308,7 @@ export declare const newestHits: (context: StoreContext, where: SQL | undefined,
292
308
  providerMetadata: string | null;
293
309
  mentions: string | null;
294
310
  starred: number | null;
311
+ delivery: number | null;
295
312
  chatTitle: string | null;
296
313
  provider: string;
297
314
  accountNativeId: string;
@@ -554,6 +571,22 @@ export declare const newestHits: (context: StoreContext, where: SQL | undefined,
554
571
  identity: undefined;
555
572
  generated: undefined;
556
573
  }, {}>;
574
+ delivery: import("drizzle-orm/sqlite-core").SQLiteColumn<{
575
+ name: string;
576
+ tableName: "messages";
577
+ dataType: "number int53";
578
+ data: number;
579
+ driverParam: number;
580
+ notNull: false;
581
+ hasDefault: false;
582
+ isPrimaryKey: false;
583
+ isAutoincrement: false;
584
+ hasRuntimeDefault: false;
585
+ enumValues: undefined;
586
+ baseColumn: never;
587
+ identity: undefined;
588
+ generated: undefined;
589
+ }, {}>;
557
590
  chatTitle: SQL.Aliased<string | null>;
558
591
  provider: SQL.Aliased<string>;
559
592
  accountNativeId: SQL.Aliased<string>;
@@ -1,3 +1,4 @@
1
+ import type { Delivery } from "../../domain/models.js";
1
2
  export declare const toMs: (iso: string | null | undefined) => number | null;
2
3
  export declare const toIso: (ms: unknown) => string | null;
3
4
  export declare const json: (value: unknown) => string | null;
@@ -5,3 +6,6 @@ export declare const parsed: <T>(text: unknown) => T | undefined;
5
6
  export declare const present: <T extends Record<string, unknown>>(entries: T) => {
6
7
  [k: string]: unknown;
7
8
  };
9
+ /** `messages.delivery` keeps a level, so a receipt can only raise it. */
10
+ export declare const DELIVERY: Record<Delivery, number>;
11
+ export declare const deliveryOf: (stored: number | null) => Delivery | undefined;
@@ -8,3 +8,6 @@ export const toIso = (ms) => (typeof ms === "number" ? new Date(ms).toISOString(
8
8
  export const json = (value) => (value === undefined ? null : JSON.stringify(value));
9
9
  export const parsed = (text) => typeof text === "string" ? JSON.parse(text) : undefined;
10
10
  export const present = (entries) => Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== null && value !== undefined));
11
+ /** `messages.delivery` keeps a level, so a receipt can only raise it. */
12
+ export const DELIVERY = { delivered: 1, read: 2 };
13
+ export const deliveryOf = (stored) => stored === DELIVERY.read ? "read" : stored === DELIVERY.delivered ? "delivered" : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.381.0",
3
+ "version": "0.382.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",