@wirecat/cli-core 0.476.0 → 0.477.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 (56) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/dist/calendar/mcp/index.d.ts +12 -12
  3. package/dist/cli/index.d.ts +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/cli/messenger/account-command.js +10 -8
  6. package/dist/cli/messenger/catch-up-answers.d.ts +14 -0
  7. package/dist/cli/messenger/chat-answers.d.ts +8 -2
  8. package/dist/cli/messenger/chat-answers.js +43 -0
  9. package/dist/cli/messenger/contacts-command.js +20 -25
  10. package/dist/cli/messenger/context.d.ts +2 -0
  11. package/dist/cli/messenger/conversation-answers.d.ts +21 -0
  12. package/dist/cli/messenger/message-answers.d.ts +21 -0
  13. package/dist/cli/messenger/message-items.js +8 -0
  14. package/dist/cli/messenger/message-writes.d.ts +42 -0
  15. package/dist/cli/messenger/people-queries.d.ts +2 -0
  16. package/dist/cli/messenger/people-queries.js +7 -1
  17. package/dist/cli/messenger/reactions-command.js +38 -2
  18. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  19. package/dist/domain/models.d.ts +22 -3
  20. package/dist/entities/attachment.d.ts +8 -0
  21. package/dist/entities/attachment.js +9 -1
  22. package/dist/entities/contact-context.d.ts +42 -0
  23. package/dist/entities/contact-read.d.ts +15 -25
  24. package/dist/entities/contact-read.js +4 -15
  25. package/dist/entities/contact.d.ts +103 -134
  26. package/dist/entities/contact.js +13 -2
  27. package/dist/entities/conversation-results.d.ts +42 -0
  28. package/dist/entities/evidence.d.ts +168 -0
  29. package/dist/entities/index.d.ts +511 -201
  30. package/dist/entities/index.js +4 -3
  31. package/dist/entities/listener.d.ts +84 -0
  32. package/dist/entities/message-context.d.ts +63 -0
  33. package/dist/entities/message-write.d.ts +84 -0
  34. package/dist/entities/message.d.ts +21 -0
  35. package/dist/entities/reaction.d.ts +12 -0
  36. package/dist/entities/reaction.js +9 -0
  37. package/dist/entities/store-records.d.ts +21 -0
  38. package/dist/errors/reasons.js +2 -0
  39. package/dist/meetings/mcp/index.d.ts +15 -15
  40. package/dist/ports/messenger.d.ts +13 -2
  41. package/dist/render/markdown.js +4 -0
  42. package/dist/render/messages.js +7 -2
  43. package/dist/services/account.d.ts +1 -1
  44. package/dist/services/account.js +1 -2
  45. package/dist/services/people.d.ts +8 -5
  46. package/dist/services/people.js +6 -3
  47. package/dist/services/person-profile.d.ts +4 -3
  48. package/dist/services/person-profile.js +14 -3
  49. package/dist/store/sqlite/manifest.js +1 -0
  50. package/dist/store/sqlite/messages.js +5 -1
  51. package/dist/store/sqlite/migrations.generated.js +6 -0
  52. package/dist/store/sqlite/reads.js +1 -0
  53. package/dist/store/sqlite/schema.d.ts +16 -0
  54. package/dist/store/sqlite/schema.js +1 -0
  55. package/package.json +1 -1
  56. package/parity.json +20 -10
@@ -86,6 +86,13 @@ export declare const storeExport: v.UnionSchema<[v.StrictObjectSchema<{
86
86
  readonly error: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
87
87
  }, undefined>, undefined>;
88
88
  readonly local_path: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
89
+ readonly contact: v.OptionalSchema<v.StrictObjectSchema<{
90
+ readonly name: v.StringSchema<undefined>;
91
+ readonly first_name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
92
+ readonly last_name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
93
+ readonly phone: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
94
+ readonly user_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
95
+ }, undefined>, undefined>;
89
96
  readonly buttons: v.OptionalSchema<v.ArraySchema<v.ArraySchema<v.StrictObjectSchema<{
90
97
  readonly kind: v.PicklistSchema<["callback", "link", "message", "chat", "contact", "location", "app", "clipboard", "other"], undefined>;
91
98
  readonly text: v.StringSchema<undefined>;
@@ -118,6 +125,13 @@ export declare const storeExport: v.UnionSchema<[v.StrictObjectSchema<{
118
125
  readonly error: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
119
126
  }, undefined>, undefined>;
120
127
  readonly local_path: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
128
+ readonly contact: v.OptionalSchema<v.StrictObjectSchema<{
129
+ readonly name: v.StringSchema<undefined>;
130
+ readonly first_name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
131
+ readonly last_name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
132
+ readonly phone: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
133
+ readonly user_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
134
+ }, undefined>, undefined>;
121
135
  readonly buttons: v.OptionalSchema<v.ArraySchema<v.ArraySchema<v.StrictObjectSchema<{
122
136
  readonly kind: v.PicklistSchema<["callback", "link", "message", "chat", "contact", "location", "app", "clipboard", "other"], undefined>;
123
137
  readonly text: v.StringSchema<undefined>;
@@ -153,6 +167,13 @@ export declare const storeExport: v.UnionSchema<[v.StrictObjectSchema<{
153
167
  readonly error: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
154
168
  }, undefined>, undefined>;
155
169
  readonly local_path: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
170
+ readonly contact: v.OptionalSchema<v.StrictObjectSchema<{
171
+ readonly name: v.StringSchema<undefined>;
172
+ readonly first_name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
173
+ readonly last_name: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
174
+ readonly phone: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
175
+ readonly user_id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
176
+ }, undefined>, undefined>;
156
177
  readonly buttons: v.OptionalSchema<v.ArraySchema<v.ArraySchema<v.StrictObjectSchema<{
157
178
  readonly kind: v.PicklistSchema<["callback", "link", "message", "chat", "contact", "location", "app", "clipboard", "other"], undefined>;
158
179
  readonly text: v.StringSchema<undefined>;
@@ -99,6 +99,8 @@ const OTHERS = [
99
99
  validation("tag-review.required", "the tag needs the owner's review first"),
100
100
  ["tag-review.stale", "configuration_error", "the tag review changed meanwhile"],
101
101
  validation("tag.invalid", "the tag is not valid"),
102
+ ["reactions.list-hidden", "permission_error", "the chat does not let its members see who reacted"],
103
+ ["reactions.broadcast", "permission_error", "a channel does not show who reacted to its posts"],
102
104
  validation("task.category-unknown", "the category is not in the task category list"),
103
105
  validation("task.claimed", "someone else holds the task"),
104
106
  validation("task.closed", "the task is already closed"),
@@ -38,6 +38,7 @@ export declare const meetingTools: ({
38
38
  max_read_bytes?: undefined;
39
39
  cursor?: undefined;
40
40
  } | {
41
+ limit?: undefined;
41
42
  event_id?: undefined;
42
43
  series_id?: undefined;
43
44
  page?: undefined;
@@ -53,11 +54,11 @@ export declare const meetingTools: ({
53
54
  format?: undefined;
54
55
  transcript_id?: undefined;
55
56
  query?: undefined;
56
- limit?: undefined;
57
57
  scan_limit?: undefined;
58
58
  max_read_bytes?: undefined;
59
59
  cursor?: undefined;
60
60
  } | {
61
+ limit?: undefined;
61
62
  event_id?: undefined;
62
63
  series_id?: undefined;
63
64
  page?: undefined;
@@ -75,11 +76,11 @@ export declare const meetingTools: ({
75
76
  format?: undefined;
76
77
  transcript_id?: undefined;
77
78
  query?: undefined;
78
- limit?: undefined;
79
79
  scan_limit?: undefined;
80
80
  max_read_bytes?: undefined;
81
81
  cursor?: undefined;
82
82
  } | {
83
+ limit?: undefined;
83
84
  event_id?: undefined;
84
85
  series_id?: undefined;
85
86
  page?: undefined;
@@ -101,7 +102,6 @@ export declare const meetingTools: ({
101
102
  minimum: number;
102
103
  };
103
104
  query?: undefined;
104
- limit?: undefined;
105
105
  scan_limit?: undefined;
106
106
  max_read_bytes?: undefined;
107
107
  cursor?: undefined;
@@ -188,6 +188,7 @@ export declare const meetingTools: ({
188
188
  max_read_bytes?: undefined;
189
189
  cursor?: undefined;
190
190
  } | {
191
+ limit?: undefined;
191
192
  event_id?: undefined;
192
193
  series_id?: undefined;
193
194
  page?: undefined;
@@ -203,11 +204,11 @@ export declare const meetingTools: ({
203
204
  format?: undefined;
204
205
  transcript_id?: undefined;
205
206
  query?: undefined;
206
- limit?: undefined;
207
207
  scan_limit?: undefined;
208
208
  max_read_bytes?: undefined;
209
209
  cursor?: undefined;
210
210
  } | {
211
+ limit?: undefined;
211
212
  event_id?: undefined;
212
213
  series_id?: undefined;
213
214
  page?: undefined;
@@ -225,11 +226,11 @@ export declare const meetingTools: ({
225
226
  format?: undefined;
226
227
  transcript_id?: undefined;
227
228
  query?: undefined;
228
- limit?: undefined;
229
229
  scan_limit?: undefined;
230
230
  max_read_bytes?: undefined;
231
231
  cursor?: undefined;
232
232
  } | {
233
+ limit?: undefined;
233
234
  event_id?: undefined;
234
235
  series_id?: undefined;
235
236
  page?: undefined;
@@ -251,7 +252,6 @@ export declare const meetingTools: ({
251
252
  minimum: number;
252
253
  };
253
254
  query?: undefined;
254
- limit?: undefined;
255
255
  scan_limit?: undefined;
256
256
  max_read_bytes?: undefined;
257
257
  cursor?: undefined;
@@ -338,6 +338,7 @@ export declare const meetingTools: ({
338
338
  max_read_bytes?: undefined;
339
339
  cursor?: undefined;
340
340
  } | {
341
+ limit?: undefined;
341
342
  event_id?: undefined;
342
343
  series_id?: undefined;
343
344
  page?: undefined;
@@ -353,11 +354,11 @@ export declare const meetingTools: ({
353
354
  format?: undefined;
354
355
  transcript_id?: undefined;
355
356
  query?: undefined;
356
- limit?: undefined;
357
357
  scan_limit?: undefined;
358
358
  max_read_bytes?: undefined;
359
359
  cursor?: undefined;
360
360
  } | {
361
+ limit?: undefined;
361
362
  event_id?: undefined;
362
363
  series_id?: undefined;
363
364
  page?: undefined;
@@ -375,11 +376,11 @@ export declare const meetingTools: ({
375
376
  format?: undefined;
376
377
  transcript_id?: undefined;
377
378
  query?: undefined;
378
- limit?: undefined;
379
379
  scan_limit?: undefined;
380
380
  max_read_bytes?: undefined;
381
381
  cursor?: undefined;
382
382
  } | {
383
+ limit?: undefined;
383
384
  event_id?: undefined;
384
385
  series_id?: undefined;
385
386
  page?: undefined;
@@ -401,7 +402,6 @@ export declare const meetingTools: ({
401
402
  minimum: number;
402
403
  };
403
404
  query?: undefined;
404
- limit?: undefined;
405
405
  scan_limit?: undefined;
406
406
  max_read_bytes?: undefined;
407
407
  cursor?: undefined;
@@ -488,6 +488,7 @@ export declare const meetingTools: ({
488
488
  max_read_bytes?: undefined;
489
489
  cursor?: undefined;
490
490
  } | {
491
+ limit?: undefined;
491
492
  event_id?: undefined;
492
493
  series_id?: undefined;
493
494
  page?: undefined;
@@ -503,11 +504,11 @@ export declare const meetingTools: ({
503
504
  format?: undefined;
504
505
  transcript_id?: undefined;
505
506
  query?: undefined;
506
- limit?: undefined;
507
507
  scan_limit?: undefined;
508
508
  max_read_bytes?: undefined;
509
509
  cursor?: undefined;
510
510
  } | {
511
+ limit?: undefined;
511
512
  event_id?: undefined;
512
513
  series_id?: undefined;
513
514
  page?: undefined;
@@ -525,11 +526,11 @@ export declare const meetingTools: ({
525
526
  format?: undefined;
526
527
  transcript_id?: undefined;
527
528
  query?: undefined;
528
- limit?: undefined;
529
529
  scan_limit?: undefined;
530
530
  max_read_bytes?: undefined;
531
531
  cursor?: undefined;
532
532
  } | {
533
+ limit?: undefined;
533
534
  event_id?: undefined;
534
535
  series_id?: undefined;
535
536
  page?: undefined;
@@ -551,7 +552,6 @@ export declare const meetingTools: ({
551
552
  minimum: number;
552
553
  };
553
554
  query?: undefined;
554
- limit?: undefined;
555
555
  scan_limit?: undefined;
556
556
  max_read_bytes?: undefined;
557
557
  cursor?: undefined;
@@ -638,6 +638,7 @@ export declare const meetingTools: ({
638
638
  max_read_bytes?: undefined;
639
639
  cursor?: undefined;
640
640
  } | {
641
+ limit?: undefined;
641
642
  event_id?: undefined;
642
643
  series_id?: undefined;
643
644
  page?: undefined;
@@ -653,11 +654,11 @@ export declare const meetingTools: ({
653
654
  format?: undefined;
654
655
  transcript_id?: undefined;
655
656
  query?: undefined;
656
- limit?: undefined;
657
657
  scan_limit?: undefined;
658
658
  max_read_bytes?: undefined;
659
659
  cursor?: undefined;
660
660
  } | {
661
+ limit?: undefined;
661
662
  event_id?: undefined;
662
663
  series_id?: undefined;
663
664
  page?: undefined;
@@ -675,11 +676,11 @@ export declare const meetingTools: ({
675
676
  format?: undefined;
676
677
  transcript_id?: undefined;
677
678
  query?: undefined;
678
- limit?: undefined;
679
679
  scan_limit?: undefined;
680
680
  max_read_bytes?: undefined;
681
681
  cursor?: undefined;
682
682
  } | {
683
+ limit?: undefined;
683
684
  event_id?: undefined;
684
685
  series_id?: undefined;
685
686
  page?: undefined;
@@ -701,7 +702,6 @@ export declare const meetingTools: ({
701
702
  minimum: number;
702
703
  };
703
704
  query?: undefined;
704
- limit?: undefined;
705
705
  scan_limit?: undefined;
706
706
  max_read_bytes?: undefined;
707
707
  cursor?: undefined;
@@ -3,7 +3,7 @@ import type { CounterField, CounterObservations } from "../domain/counters.js";
3
3
  import type { HtmlFormatting, MarkdownFormatting, TextSpan } from "../domain/formatting.js";
4
4
  import type { Markup } from "../domain/markdown.js";
5
5
  import type { MessagePermalink } from "../domain/message-link.js";
6
- import type { Account, AccountSession, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, ChatUpdate, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PersonReaction, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
6
+ import type { Account, AccountSession, AdminRight, Button, CallRecord, Chat, ChatCard, ChatEvent, ChatEvents, ChatUpdate, Discussion, Folder, FolderChange, FolderRules, GroupCard, GroupChange, GroupMember, Id, InviteLink, InviteLinkChange, JoinRequest, LinkTarget, MediaKind, Member, Message, MessageEvent, MessageHit, OfficialChatStats, Page, PersonCard, PersonReaction, PhoneBookEntry, Poll, PollVote, PrivacySettings, ProfileFacts, ProviderMetadata, Reactor, SenderIdentity, Sticker, StickerSet, Topic, TopicChange, WindowedMessage } from "../domain/models.js";
7
7
  /** One attachment's bytes, fetched over the adapter's connection when `bytes` is read — so read it before the command closes. */
8
8
  export interface RemoteFile {
9
9
  kind: string;
@@ -237,6 +237,17 @@ export interface MessageStars {
237
237
  export interface MessageReactions {
238
238
  /** The owner's reaction on one message: an emoji replaces the one there was, `null` takes it off. */
239
239
  react(chatId: Id, messageId: Id, emoji: string | null): Promise<void>;
240
+ /**
241
+ * Who reacted, newest first; `emoji` keeps one reaction, `cursor` is the last page's. Refuses with a
242
+ * `reactions.*` reason where the messenger hides the list, rather than answering with none.
243
+ */
244
+ reactors?(chatId: Id, messageId: Id, window: {
245
+ limit: number;
246
+ emoji?: string;
247
+ cursor?: string;
248
+ }): Promise<Page<Reactor> & {
249
+ cursor?: string;
250
+ }>;
240
251
  }
241
252
  export interface ReadState {
242
253
  /** Marks the chat read up to `until`, or to its newest message; the other side sees it. */
@@ -493,7 +504,7 @@ export interface AccountTools {
493
504
  /** The owner's contact list as the messenger keeps it — the address book, not the chats. */
494
505
  addressBook(): Promise<Member[]>;
495
506
  }
496
- /** Everything the messenger says about one person — what `contacts info` shows. */
507
+ /** Everything the messenger says about one person — what `contacts show` shows. */
497
508
  export interface PersonProfiles {
498
509
  /** Reading it tells them nothing. A chat that is not a person is refused. */
499
510
  profile(person: string): Promise<ProfileFacts>;
@@ -51,6 +51,10 @@ const attachmentLine = (attachment) => {
51
51
  const buttons = attachment.buttons.flat().map((button, index) => `${index + 1} ${literal(singleLine(button.text))}`);
52
52
  return `buttons: ${buttons.join(" · ")}`;
53
53
  }
54
+ if (attachment.contact) {
55
+ const { name, phone } = attachment.contact;
56
+ return `contact: ${[name, phone].flatMap((part) => (part ? [literal(singleLine(part))] : [])).join(", ")}`;
57
+ }
54
58
  const label = literal(singleLine(attachment.title ?? attachment.name ?? attachment.kind));
55
59
  if (attachment.url) {
56
60
  try {
@@ -119,9 +119,14 @@ const attachmentLines = (attachments, paint, options) => {
119
119
  .flatMap((attachment) => attachment.buttons ?? [])
120
120
  .map((row) => paint("dim", row.map((button) => link(`[${++number} ${singleLine(button.text)}]`, button.url)).join(" ")));
121
121
  const polls = attachments.flatMap((attachment) => (attachment.poll ? pollLines(attachment.poll) : []));
122
+ const cards = attachments.flatMap(({ contact }) => contact
123
+ ? [
124
+ paint("dim", `👤 ${[contact.name, contact.phone].flatMap((part) => (part ? [singleLine(part)] : [])).join(" · ")}`),
125
+ ]
126
+ : []);
122
127
  const groups = new Map();
123
128
  for (const attachment of attachments) {
124
- if (attachment.buttons || attachment.poll)
129
+ if (attachment.buttons || attachment.poll || attachment.contact)
125
130
  continue;
126
131
  const key = singleLine(attachment.name ?? attachment.title ?? attachment.kind);
127
132
  groups.set(key, [...(groups.get(key) ?? []), attachment]);
@@ -134,7 +139,7 @@ const attachmentLines = (attachments, paint, options) => {
134
139
  const each = same.map((attachment, index) => link(String(index + 1), attachment.url)).join(" ");
135
140
  return paint("dim", `📎 ${label} ×${same.length} ${each}`);
136
141
  });
137
- return [...polls, ...files, ...keyboard];
142
+ return [...polls, ...cards, ...files, ...keyboard];
138
143
  };
139
144
  /** `📊 question`, then each answer with the id a vote takes, its voters and ✓ on the owner's own, then the totals. */
140
145
  export const pollLines = (poll) => {
@@ -4,7 +4,7 @@ import { type Operated } from "../sends/guarded.js";
4
4
  import type { ServiceDeps } from "./deps.js";
5
5
  /** The owner's own profile and logins. */
6
6
  export interface AccountService {
7
- /** The account as it now stands, its phone masked as `account show` masks it. */
7
+ /** The account as it now stands. */
8
8
  update(change: ProfileChange): Promise<Operated<{
9
9
  account: Account;
10
10
  }>>;
@@ -2,7 +2,6 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { capability } from "../ports/messenger.js";
3
3
  import { guardedWrite } from "../sends/guarded.js";
4
4
  import { newOperationId } from "../sends/send-id.js";
5
- import { maskedAccount } from "./people.js";
6
5
  export const accountService = (deps) => {
7
6
  const online = async (command) => {
8
7
  if (deps.offline)
@@ -28,7 +27,7 @@ export const accountService = (deps) => {
28
27
  const update = capability(await online("account update"), "updateProfile", "change the profile");
29
28
  const operationId = newOperationId();
30
29
  const account = await guardedWrite(deps.guard, { operationId, chatId: null, kind: "account", action: "profile" }, () => update(change));
31
- return { operationId, account: maskedAccount(account) };
30
+ return { operationId, account };
32
31
  },
33
32
  endOtherSessions: async () => {
34
33
  const end = capability(await online("account sessions end"), "endOtherSessions", "end other sessions");
@@ -30,8 +30,11 @@ export interface PeopleService {
30
30
  show(person: string, options?: {
31
31
  notes?: boolean;
32
32
  }): Promise<PersonCard>;
33
- /** What the messenger says about them, and their stored activity in each shared chat. */
34
- profile(person: string): Promise<PersonProfile>;
33
+ /**
34
+ * What the messenger says about them, and their stored activity in each shared chat. `known`: their card, already
35
+ * read, which stands in for a messenger that has no profile to give.
36
+ */
37
+ profile(person: string, known?: PersonCard): Promise<PersonProfile>;
35
38
  /** `phone` as digits, parsed by the caller (`phoneOf`). */
36
39
  lookup(phone: string): Promise<Member>;
37
40
  /** The whole contact list from the messenger into the store: what was new, what changed. */
@@ -89,7 +92,7 @@ export declare const peopleService: (deps: ServiceDeps) => PeopleService;
89
92
  */
90
93
  export declare const linkPeople: (store: MessageStore, account: AccountKey, person: string, other: string, method?: string) => Promise<PersonRecord>;
91
94
  export declare const phoneOf: (typed: string) => string;
92
- /** The phone cut to its last four digits: enough to tell two accounts apart. */
93
- export declare const maskedAccount: <T extends {
95
+ /** `--hide-phone`: the answer without the number, for output that will be pasted somewhere. */
96
+ export declare const withoutPhone: <T extends {
94
97
  phone?: string | null;
95
- }>(account: T) => T;
98
+ }>(answer: T) => T;
@@ -74,7 +74,7 @@ export const peopleService = (deps) => {
74
74
  const held = await storeIfOpen(deps);
75
75
  return localCard(held ? { ...card, chats: await sharedChats(held.store, held.account, card.id) } : card, notes);
76
76
  },
77
- profile: (person) => personProfile(deps, person),
77
+ profile: (person, known) => personProfile(deps, person, known),
78
78
  context: async (person, options) => personContext(await deps.store(), await deps.account(), person, options),
79
79
  prepare: async (person, options) => personPreparation(await deps.store(), await deps.account(), person, options),
80
80
  timeline: async (person, options) => personTimeline(await deps.store(), await deps.account(), person, options),
@@ -256,5 +256,8 @@ const toContact = (chat) => ({
256
256
  });
257
257
  const byRecency = (a, b) => (b.lastMessagedAt ?? "").localeCompare(a.lastMessagedAt ?? "");
258
258
  const byName = (a, b) => (a.name ?? "").localeCompare(b.name ?? "");
259
- /** The phone cut to its last four digits: enough to tell two accounts apart. */
260
- export const maskedAccount = (account) => account.phone ? { ...account, phone: `***${account.phone.replace(/\D/g, "").slice(-4)}` } : account;
259
+ /** `--hide-phone`: the answer without the number, for output that will be pasted somewhere. */
260
+ export const withoutPhone = (answer) => {
261
+ const { phone: _phone, ...rest } = answer;
262
+ return rest;
263
+ };
@@ -1,9 +1,10 @@
1
- import type { PersonProfile } from "../domain/models.js";
1
+ import type { PersonCard, PersonProfile } from "../domain/models.js";
2
2
  import { type ServiceDeps } from "./deps.js";
3
3
  /**
4
4
  * The person as the messenger describes them, and their stored activity in every chat shared with
5
5
  * them. Online, one profile read; `--offline`, or a messenger that cannot describe a person, what
6
6
  * the store has seen of them. Counts always come from the store, so they are a floor unless the
7
- * chat is stored whole.
7
+ * chat is stored whole. `known` is the card already read for them: where the messenger has no profile to give, it is
8
+ * what is known, so a person the store has not saved is not looked up in it a second time.
8
9
  */
9
- export declare const personProfile: (deps: ServiceDeps, person: string) => Promise<PersonProfile>;
10
+ export declare const personProfile: (deps: ServiceDeps, person: string, known?: PersonCard) => Promise<PersonProfile>;
@@ -4,14 +4,17 @@ import { storeIfOpen } from "./deps.js";
4
4
  * The person as the messenger describes them, and their stored activity in every chat shared with
5
5
  * them. Online, one profile read; `--offline`, or a messenger that cannot describe a person, what
6
6
  * the store has seen of them. Counts always come from the store, so they are a floor unless the
7
- * chat is stored whole.
7
+ * chat is stored whole. `known` is the card already read for them: where the messenger has no profile to give, it is
8
+ * what is known, so a person the store has not saved is not looked up in it a second time.
8
9
  */
9
- export const personProfile = async (deps, person) => {
10
+ export const personProfile = async (deps, person, known) => {
10
11
  const online = deps.offline ? undefined : await deps.connection();
11
12
  const held = await storeIfOpen(deps);
12
13
  const facts = online?.profile
13
14
  ? await online.profile(person)
14
- : await storedFacts(held ?? { store: await deps.store(), account: await deps.account() }, person);
15
+ : known
16
+ ? factsOfCard(known)
17
+ : await storedFacts(held ?? { store: await deps.store(), account: await deps.account() }, person);
15
18
  if (!held)
16
19
  return { ...facts, chats: quiet(facts), aliases: [] };
17
20
  return {
@@ -25,6 +28,14 @@ const pastOf = (facts, recorded, provider) => recorded
25
28
  .filter(({ name, username }) => (name ?? null) !== facts.name || (username !== undefined && !facts.usernames.includes(username)))
26
29
  .map((alias) => alias.username && provider === "telegram" ? { ...alias, link: `https://t.me/${alias.username}` } : alias)
27
30
  .sort((a, b) => a.firstSeenAt.localeCompare(b.firstSeenAt));
31
+ const factsOfCard = ({ id, name, username, description, chats }) => ({
32
+ id,
33
+ name,
34
+ usernames: username ? [username] : [],
35
+ bio: description,
36
+ flags: {},
37
+ chats,
38
+ });
28
39
  const storedFacts = async ({ store, account }, person) => {
29
40
  const found = pickPerson(person, await store.people.people(account.provider, { account: account.account }));
30
41
  const chats = await store.chats.chatsWith(account, found.id);
@@ -47,6 +47,7 @@ export const MANIFEST = [
47
47
  { name: "20261025182501_version-34-vector-index-triggers", version: 34, minCompatible: 34 },
48
48
  { name: "20261027102259_version-35-calendar-neutral", version: 35, minCompatible: 34 },
49
49
  { name: "20261027102302_version-35-calendar-neutral-data", version: 35, minCompatible: 34 },
50
+ { name: "20261029162435_version-36-attachment-contact", version: 36, minCompatible: 34 },
50
51
  ];
51
52
  export const generatedMigrations = (manifest = MANIFEST, generated = GENERATED) => {
52
53
  const migrations = [];
@@ -38,6 +38,7 @@ const ATTACHMENT_FIELDS = [
38
38
  "duration",
39
39
  "providerRef",
40
40
  "nameFolded",
41
+ "contact",
41
42
  ];
42
43
  const placeholders = (names) => Object.fromEntries(names.map((name) => [name, sql.placeholder(name)]));
43
44
  const prepare = (orm) => ({
@@ -78,7 +79,9 @@ const prepare = (orm) => ({
78
79
  const column = attachments[name].name;
79
80
  return [
80
81
  name,
81
- sql.raw(name === "providerRef" ? `coalesce(excluded.${column}, ${column})` : `excluded.${column}`),
82
+ sql.raw(name === "providerRef" || name === "contact"
83
+ ? `coalesce(excluded.${column}, ${column})`
84
+ : `excluded.${column}`),
82
85
  ];
83
86
  })),
84
87
  })
@@ -224,6 +227,7 @@ export const upsertMessage = (context, key, accountKey, chatKey, message, via, s
224
227
  height: attachment.height ?? null,
225
228
  duration: attachment.duration ?? null,
226
229
  providerRef: refOf(attachment.providerRef),
230
+ contact: attachment.contact ? JSON.stringify(attachment.contact) : null,
227
231
  });
228
232
  });
229
233
  if (Number(heardFirst) > 0)
@@ -778,5 +778,11 @@ export const GENERATED = [
778
778
  "UPDATE calendar_entry_participants SET response = 'needs-action' WHERE response = 'needsAction';",
779
779
  "UPDATE calendars SET access_role = 'free-busy' WHERE access_role = 'freeBusyReader';"
780
780
  ]
781
+ },
782
+ {
783
+ "name": "20261029162435_version-36-attachment-contact",
784
+ "statements": [
785
+ "ALTER TABLE `attachments` ADD `contact` text;"
786
+ ]
781
787
  }
782
788
  ];
@@ -215,6 +215,7 @@ const toAttachment = (row) => ({
215
215
  mime: row.mime,
216
216
  duration: row.duration,
217
217
  providerRef: parsed(row.providerRef),
218
+ contact: parsed(row.contact),
218
219
  }),
219
220
  });
220
221
  const toMessage = (row, attachments) => {
@@ -6552,6 +6552,22 @@ export declare const attachments: import("drizzle-orm/sqlite-core").SQLiteTableW
6552
6552
  identity: undefined;
6553
6553
  generated: undefined;
6554
6554
  }, {}>;
6555
+ contact: import("drizzle-orm/sqlite-core").SQLiteColumn<{
6556
+ name: string;
6557
+ tableName: "attachments";
6558
+ dataType: "string";
6559
+ data: string;
6560
+ driverParam: string;
6561
+ notNull: false;
6562
+ hasDefault: false;
6563
+ isPrimaryKey: false;
6564
+ isAutoincrement: false;
6565
+ hasRuntimeDefault: false;
6566
+ enumValues: [string, ...string[]];
6567
+ baseColumn: never;
6568
+ identity: undefined;
6569
+ generated: undefined;
6570
+ }, {}>;
6555
6571
  };
6556
6572
  dialect: 'sqlite';
6557
6573
  isAlias: false;
@@ -713,6 +713,7 @@ export const attachments = sqliteTable("attachments", {
713
713
  /** `fold(name)`, `fold(local_path)`: SQLite's lower() folds ASCII only, so filters compare these. */
714
714
  nameFolded: text("name_folded"),
715
715
  localPathFolded: text("local_path_folded"),
716
+ contact: text("contact"),
716
717
  }, (table) => [
717
718
  unique().on(table.attachableType, table.attachableId, table.position),
718
719
  index("attachments_by_name_folded").on(table.nameFolded),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.476.0",
3
+ "version": "0.477.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",
package/parity.json CHANGED
@@ -433,6 +433,9 @@
433
433
  "value": "<on|off>",
434
434
  "meaning": "hide online status and last seen"
435
435
  },
436
+ "--hide-phone": {
437
+ "meaning": "leave the phone number out"
438
+ },
436
439
  "--history": {
437
440
  "meaning": "the people added also see the messages from before they came"
438
441
  },
@@ -906,9 +909,6 @@
906
909
  "value": "<id>",
907
910
  "meaning": "the stickers in this set"
908
911
  },
909
- "--show-phone": {
910
- "meaning": "print the whole phone number"
911
- },
912
912
  "--silent": {
913
913
  "meaning": "deliver without a notification"
914
914
  },
@@ -1268,7 +1268,7 @@
1268
1268
  "account show": {
1269
1269
  "in": "all",
1270
1270
  "options": {
1271
- "--show-phone": "all"
1271
+ "--hide-phone": "all"
1272
1272
  }
1273
1273
  },
1274
1274
  "account update": {
@@ -1276,6 +1276,7 @@
1276
1276
  "options": {
1277
1277
  "--description": "all",
1278
1278
  "--first-name": "all",
1279
+ "--hide-phone": "all",
1279
1280
  "--last-name": "all",
1280
1281
  "--photo": {
1281
1282
  "in": "all",
@@ -2803,12 +2804,6 @@
2803
2804
  "contacts import": {
2804
2805
  "in": "all"
2805
2806
  },
2806
- "contacts info": {
2807
- "in": "all",
2808
- "options": {
2809
- "--show-phone": "all"
2810
- }
2811
- },
2812
2807
  "contacts link": {
2813
2808
  "in": "all"
2814
2809
  },
@@ -2868,6 +2863,7 @@
2868
2863
  "contacts show": {
2869
2864
  "in": "all",
2870
2865
  "options": {
2866
+ "--hide-phone": "all",
2871
2867
  "--with-notes": {
2872
2868
  "in": "all"
2873
2869
  }
@@ -3674,6 +3670,20 @@
3674
3670
  "reactions add": {
3675
3671
  "in": "all"
3676
3672
  },
3673
+ "reactions list": {
3674
+ "in": ["tg"],
3675
+ "reason": "max-cli and whatsapp-cli do not list who reacted to a message",
3676
+ "options": {
3677
+ "--cursor": {
3678
+ "in": ["tg"],
3679
+ "reason": "max-cli and whatsapp-cli do not list who reacted to a message"
3680
+ },
3681
+ "--limit": {
3682
+ "in": ["tg"],
3683
+ "reason": "max-cli and whatsapp-cli do not list who reacted to a message"
3684
+ }
3685
+ }
3686
+ },
3677
3687
  "reactions remove": {
3678
3688
  "in": "all"
3679
3689
  },