@wirecat/cli-core 0.404.0 → 0.406.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 (124) hide show
  1. package/CHANGELOG.md +45 -1
  2. package/dist/botcheck/reasons.d.ts +1 -1
  3. package/dist/cli/analysis-choice.js +1 -1
  4. package/dist/cli/answer-entity.d.ts +5 -0
  5. package/dist/cli/answer-entity.js +25 -0
  6. package/dist/cli/command-contract.js +2 -5
  7. package/dist/cli/commands-command.js +4 -1
  8. package/dist/cli/effects.js +1 -1
  9. package/dist/cli/embedding-choice.d.ts +1 -1
  10. package/dist/cli/embedding-choice.js +2 -2
  11. package/dist/cli/first-run.js +3 -3
  12. package/dist/cli/help/command-help.js +2 -2
  13. package/dist/cli/index.d.ts +1 -0
  14. package/dist/cli/index.js +1 -0
  15. package/dist/cli/json-fields.js +2 -2
  16. package/dist/cli/meeting-project-candidates-command.js +0 -1
  17. package/dist/cli/meeting-series-inventory-command.js +0 -1
  18. package/dist/cli/messenger/admin-commands.js +14 -14
  19. package/dist/cli/messenger/archive-commands.js +2 -2
  20. package/dist/cli/messenger/attachments-command.js +7 -8
  21. package/dist/cli/messenger/backfill-command.js +1 -1
  22. package/dist/cli/messenger/chat-answers.js +3 -3
  23. package/dist/cli/messenger/chat-lists.js +3 -3
  24. package/dist/cli/messenger/chats-command.js +8 -8
  25. package/dist/cli/messenger/chats-members-command.js +1 -1
  26. package/dist/cli/messenger/contacts-command.js +10 -7
  27. package/dist/cli/messenger/context.d.ts +11 -4
  28. package/dist/cli/messenger/context.js +6 -0
  29. package/dist/cli/messenger/conversation-examples.js +7 -6
  30. package/dist/cli/messenger/conversations-command.d.ts +0 -4
  31. package/dist/cli/messenger/conversations-command.js +130 -135
  32. package/dist/cli/messenger/counters-command.js +4 -2
  33. package/dist/cli/messenger/doctor-command.js +8 -4
  34. package/dist/cli/messenger/gaps-command.js +2 -2
  35. package/dist/cli/messenger/hearing-command.d.ts +1 -1
  36. package/dist/cli/messenger/hearing-command.js +1 -1
  37. package/dist/cli/messenger/images-command.js +1 -1
  38. package/dist/cli/messenger/inbox.js +1 -3
  39. package/dist/cli/messenger/lacks.d.ts +1 -0
  40. package/dist/cli/messenger/lacks.js +1 -1
  41. package/dist/cli/messenger/mcp-command.js +4 -4
  42. package/dist/cli/messenger/messages-comments-command.js +10 -5
  43. package/dist/cli/messenger/messages-send-command.js +44 -3
  44. package/dist/cli/messenger/metadata-command.js +5 -5
  45. package/dist/cli/messenger/models-command.js +101 -90
  46. package/dist/cli/messenger/private-people-command.js +5 -6
  47. package/dist/cli/messenger/replies-edit-command.js +0 -1
  48. package/dist/cli/messenger/review.js +1 -3
  49. package/dist/cli/messenger/serve-relay.d.ts +2 -0
  50. package/dist/cli/messenger/stats-command.js +1 -1
  51. package/dist/cli/messenger/store-maintenance-command.d.ts +2 -2
  52. package/dist/cli/messenger/store-maintenance-command.js +4 -4
  53. package/dist/cli/messenger/stored.js +5 -0
  54. package/dist/cli/messenger/tags-command.js +4 -10
  55. package/dist/cli/messenger/tasks-command.js +3 -3
  56. package/dist/cli/messenger/transcribe-command.js +1 -1
  57. package/dist/cli/moved.d.ts +2 -0
  58. package/dist/cli/moved.js +39 -1
  59. package/dist/cli/permission-keys.js +2 -0
  60. package/dist/cli/settings.js +1 -1
  61. package/dist/cli/setup-answers.d.ts +2 -2
  62. package/dist/cli/setup-answers.js +2 -2
  63. package/dist/cli/steps.js +4 -4
  64. package/dist/cli/store-settings.js +1 -1
  65. package/dist/cli/upgrade-command.js +0 -1
  66. package/dist/domain/models.d.ts +3 -3
  67. package/dist/domain/task-categories.js +1 -1
  68. package/dist/embeddings/remote.js +1 -1
  69. package/dist/entities/index.d.ts +380 -0
  70. package/dist/entities/index.js +2 -1
  71. package/dist/entities/member.d.ts +2 -2
  72. package/dist/entities/member.js +2 -2
  73. package/dist/entities/message-write.d.ts +386 -0
  74. package/dist/entities/message-write.js +6 -0
  75. package/dist/mcp/agent.d.ts +1 -0
  76. package/dist/mcp/agent.js +1 -1
  77. package/dist/mcp/tools/attachments.js +1 -1
  78. package/dist/meetings/mcp/index.d.ts +15 -15
  79. package/dist/ports/app.d.ts +1 -1
  80. package/dist/ports/messenger.d.ts +8 -1
  81. package/dist/ports/traits.d.ts +2 -0
  82. package/dist/search/task-similarity.js +1 -1
  83. package/dist/sends/guard.js +2 -2
  84. package/dist/sends/permission-aliases.d.ts +5 -0
  85. package/dist/sends/permission-aliases.js +49 -0
  86. package/dist/sends/permissions.d.ts +1 -1
  87. package/dist/sends/permissions.js +35 -25
  88. package/dist/services/admin.js +7 -7
  89. package/dist/services/chats.js +1 -1
  90. package/dist/services/conversations.d.ts +1 -1
  91. package/dist/services/conversations.js +1 -1
  92. package/dist/services/counters.js +1 -1
  93. package/dist/services/embeddings-maintenance.js +2 -2
  94. package/dist/services/embeddings-search.js +1 -1
  95. package/dist/services/embeddings.d.ts +4 -4
  96. package/dist/services/messages-search.d.ts +1 -1
  97. package/dist/services/messages-search.js +2 -2
  98. package/dist/services/messages.d.ts +5 -0
  99. package/dist/services/messages.js +23 -0
  100. package/dist/services/metadata.js +2 -2
  101. package/dist/services/notes-search.js +3 -3
  102. package/dist/services/search-catchup.js +1 -1
  103. package/dist/speech/install.d.ts +4 -2
  104. package/dist/speech/install.js +16 -4
  105. package/dist/speech/local.js +1 -1
  106. package/dist/speech/transcribe.js +1 -1
  107. package/dist/store/search-index.d.ts +1 -1
  108. package/dist/store/sqlite/attachment-texts.d.ts +1 -1
  109. package/dist/store/sqlite/attachment-texts.js +1 -1
  110. package/dist/store/sqlite/batches.js +1 -1
  111. package/dist/store/sqlite/involvements.d.ts +1 -1
  112. package/dist/store/sqlite/note-index.d.ts +2 -2
  113. package/dist/store/sqlite/note-index.js +2 -2
  114. package/dist/store/sqlite/search-index.d.ts +1 -1
  115. package/dist/store/sqlite/search-index.js +1 -1
  116. package/dist/store/sqlite/stems-fill.d.ts +3 -3
  117. package/dist/store/sqlite/stems-fill.js +3 -3
  118. package/dist/store/sqlite/stems.d.ts +1 -1
  119. package/dist/store/sqlite/task-pairs.d.ts +1 -1
  120. package/dist/store/sqlite/task-pairs.js +1 -1
  121. package/package.json +1 -1
  122. package/parity.json +186 -179
  123. package/skills/follow-ups/SKILL.md +5 -5
  124. package/skills/link-conversations/SKILL.md +8 -8
@@ -38,7 +38,6 @@ export declare const meetingTools: ({
38
38
  max_read_bytes?: undefined;
39
39
  cursor?: undefined;
40
40
  } | {
41
- limit?: undefined;
42
41
  event_id?: undefined;
43
42
  series_id?: undefined;
44
43
  page?: undefined;
@@ -54,11 +53,11 @@ export declare const meetingTools: ({
54
53
  format?: undefined;
55
54
  transcript_id?: undefined;
56
55
  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;
62
61
  event_id?: undefined;
63
62
  series_id?: undefined;
64
63
  page?: undefined;
@@ -76,11 +75,11 @@ export declare const meetingTools: ({
76
75
  format?: undefined;
77
76
  transcript_id?: undefined;
78
77
  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;
84
83
  event_id?: undefined;
85
84
  series_id?: undefined;
86
85
  page?: undefined;
@@ -102,6 +101,7 @@ export declare const meetingTools: ({
102
101
  minimum: number;
103
102
  };
104
103
  query?: undefined;
104
+ limit?: undefined;
105
105
  scan_limit?: undefined;
106
106
  max_read_bytes?: undefined;
107
107
  cursor?: undefined;
@@ -188,7 +188,6 @@ export declare const meetingTools: ({
188
188
  max_read_bytes?: undefined;
189
189
  cursor?: undefined;
190
190
  } | {
191
- limit?: undefined;
192
191
  event_id?: undefined;
193
192
  series_id?: undefined;
194
193
  page?: undefined;
@@ -204,11 +203,11 @@ export declare const meetingTools: ({
204
203
  format?: undefined;
205
204
  transcript_id?: undefined;
206
205
  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;
212
211
  event_id?: undefined;
213
212
  series_id?: undefined;
214
213
  page?: undefined;
@@ -226,11 +225,11 @@ export declare const meetingTools: ({
226
225
  format?: undefined;
227
226
  transcript_id?: undefined;
228
227
  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;
234
233
  event_id?: undefined;
235
234
  series_id?: undefined;
236
235
  page?: undefined;
@@ -252,6 +251,7 @@ export declare const meetingTools: ({
252
251
  minimum: number;
253
252
  };
254
253
  query?: undefined;
254
+ limit?: undefined;
255
255
  scan_limit?: undefined;
256
256
  max_read_bytes?: undefined;
257
257
  cursor?: undefined;
@@ -338,7 +338,6 @@ export declare const meetingTools: ({
338
338
  max_read_bytes?: undefined;
339
339
  cursor?: undefined;
340
340
  } | {
341
- limit?: undefined;
342
341
  event_id?: undefined;
343
342
  series_id?: undefined;
344
343
  page?: undefined;
@@ -354,11 +353,11 @@ export declare const meetingTools: ({
354
353
  format?: undefined;
355
354
  transcript_id?: undefined;
356
355
  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;
362
361
  event_id?: undefined;
363
362
  series_id?: undefined;
364
363
  page?: undefined;
@@ -376,11 +375,11 @@ export declare const meetingTools: ({
376
375
  format?: undefined;
377
376
  transcript_id?: undefined;
378
377
  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;
384
383
  event_id?: undefined;
385
384
  series_id?: undefined;
386
385
  page?: undefined;
@@ -402,6 +401,7 @@ export declare const meetingTools: ({
402
401
  minimum: number;
403
402
  };
404
403
  query?: undefined;
404
+ limit?: undefined;
405
405
  scan_limit?: undefined;
406
406
  max_read_bytes?: undefined;
407
407
  cursor?: undefined;
@@ -488,7 +488,6 @@ export declare const meetingTools: ({
488
488
  max_read_bytes?: undefined;
489
489
  cursor?: undefined;
490
490
  } | {
491
- limit?: undefined;
492
491
  event_id?: undefined;
493
492
  series_id?: undefined;
494
493
  page?: undefined;
@@ -504,11 +503,11 @@ export declare const meetingTools: ({
504
503
  format?: undefined;
505
504
  transcript_id?: undefined;
506
505
  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;
512
511
  event_id?: undefined;
513
512
  series_id?: undefined;
514
513
  page?: undefined;
@@ -526,11 +525,11 @@ export declare const meetingTools: ({
526
525
  format?: undefined;
527
526
  transcript_id?: undefined;
528
527
  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;
534
533
  event_id?: undefined;
535
534
  series_id?: undefined;
536
535
  page?: undefined;
@@ -552,6 +551,7 @@ export declare const meetingTools: ({
552
551
  minimum: number;
553
552
  };
554
553
  query?: undefined;
554
+ limit?: undefined;
555
555
  scan_limit?: undefined;
556
556
  max_read_bytes?: undefined;
557
557
  cursor?: undefined;
@@ -638,7 +638,6 @@ export declare const meetingTools: ({
638
638
  max_read_bytes?: undefined;
639
639
  cursor?: undefined;
640
640
  } | {
641
- limit?: undefined;
642
641
  event_id?: undefined;
643
642
  series_id?: undefined;
644
643
  page?: undefined;
@@ -654,11 +653,11 @@ export declare const meetingTools: ({
654
653
  format?: undefined;
655
654
  transcript_id?: undefined;
656
655
  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;
662
661
  event_id?: undefined;
663
662
  series_id?: undefined;
664
663
  page?: undefined;
@@ -676,11 +675,11 @@ export declare const meetingTools: ({
676
675
  format?: undefined;
677
676
  transcript_id?: undefined;
678
677
  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;
684
683
  event_id?: undefined;
685
684
  series_id?: undefined;
686
685
  page?: undefined;
@@ -702,6 +701,7 @@ export declare const meetingTools: ({
702
701
  minimum: number;
703
702
  };
704
703
  query?: undefined;
704
+ limit?: undefined;
705
705
  scan_limit?: undefined;
706
706
  max_read_bytes?: undefined;
707
707
  cursor?: undefined;
@@ -10,7 +10,7 @@ export interface AppIdentity {
10
10
  envPrefix: string;
11
11
  description: string;
12
12
  version: string;
13
- /** Where a problem report goes — a new-issue page. `doctor report create` names it. */
13
+ /** Where a problem report goes — a new-issue page. `doctor reports create` names it. */
14
14
  issues?: string;
15
15
  /** The language of the transcript a person reads: its day headings and the word for "you". `ru-RU` when unset. */
16
16
  locale?: string;
@@ -110,6 +110,8 @@ export interface MessengerCore {
110
110
  chat(chat: string): Promise<ChatCard>;
111
111
  /** An option the messenger has no way to honour is refused, never dropped. */
112
112
  send(chatId: Id, text: string, options: SendOptions): Promise<Sent>;
113
+ /** Save a nonempty draft for the owner to review and send; never sends a message. */
114
+ saveDraft?(chatId: Id, text: string, options: Pick<SendOptions, "replyTo" | "threadId" | "formatting" | "noPreview">): Promise<void>;
113
115
  logout(): Promise<void>;
114
116
  close(): Promise<void>;
115
117
  }
@@ -375,6 +377,11 @@ export interface HistoryBatch {
375
377
  votes?: HistoryVote[];
376
378
  reactions?: HistoryReaction[];
377
379
  stars?: HistoryStar[];
380
+ /** Deletions learned from a synced snapshot, scoped to their chat and applied after the batch's messages. */
381
+ deletions?: {
382
+ chatId: Id;
383
+ messageId: Id;
384
+ }[];
378
385
  events?: HistoryChatEvent[];
379
386
  }
380
387
  /** For a messenger that pushes its history to the client instead of answering for it (`Messenger.history: "store"`). */
@@ -486,7 +493,7 @@ export interface AccountTools {
486
493
  /** The owner's contact list as the messenger keeps it — the address book, not the chats. */
487
494
  addressBook(): Promise<Member[]>;
488
495
  }
489
- /** Everything the messenger says about one person — what `contacts profile` shows. */
496
+ /** Everything the messenger says about one person — what `contacts info` shows. */
490
497
  export interface PersonProfiles {
491
498
  /** Reading it tells them nothing. A chat that is not a person is refused. */
492
499
  profile(person: string): Promise<ProfileFacts>;
@@ -24,6 +24,8 @@ export interface Fetching {
24
24
  export type MediaOption = "spoiler" | "captionAbove" | "fileName";
25
25
  /** What a messenger is and can do, as the services read it; the CLI's `Messenger` adds how its commands run. */
26
26
  export interface MessengerTraits {
27
+ /** Opt in to `messages send --draft` only when the adapter implements `saveDraft`. */
28
+ drafts?: boolean;
27
29
  counterFields?: readonly CounterField[];
28
30
  app: AppIdentity;
29
31
  provider: Provider;
@@ -2,7 +2,7 @@ import { normalize } from "../store/normalize.js";
2
2
  import { createStemmer, DEFAULT_STEMMERS } from "./stem.js";
3
3
  import { amountOf, dayOf, dueDayOf } from "./task-signature.js";
4
4
  /**
5
- * How alike two tasks are, field by field, as the tasks plan's duplicate chain scores them
5
+ * How alike two tasks are, field by field, as the tasks update's duplicate chain scores them
6
6
  * (`docs/dev/tasks-plan.md`, "Duplicates — the comparison chain" § 1, 4, 5). Ported from the scorer that
7
7
  * tuned the weights, WireCatLabs/cli-testing `performance/tasks/duplicates/` (cli-testing#60); the counts
8
8
  * quoted beside each weight are that run's duplicate / other candidate pairs at the level.
@@ -12,12 +12,12 @@ const HOUR_MS = 60 * 60 * 1000;
12
12
  */
13
13
  const READS_UNDER_HOLD = new Set([
14
14
  "attachments.extract",
15
- "conversations.embed",
15
+ "conversations.embeddings",
16
16
  "conversations.links",
17
17
  "messages.server-search",
18
18
  "messages.sync-first",
19
19
  "stats.contacts.top.sync-first",
20
- "stats.messages.counters.refresh",
20
+ "stats.messages.counters.sync",
21
21
  "stats.messages.show.sync-first",
22
22
  "stats.messages.top.sync-first",
23
23
  "store.gaps.repair",
@@ -0,0 +1,5 @@
1
+ import type { Level } from "./permissions.js";
2
+ /** Canonical names for internal permission keys; saved moved paths are refused below. */
3
+ export declare const canonicalPermissionKey: (key: string) => string;
4
+ /** WC-PERM-8: a moved saved path must name the replacement before any command runs. */
5
+ export declare const canonicalPermissions: (levels: Readonly<Record<string, Level>>) => Record<string, Level>;
@@ -0,0 +1,49 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ const RENAMED = [
3
+ ["models.audio.download", "models.download"],
4
+ ["models.text.download", "models.download"],
5
+ ["models.text.key", "models.keys"],
6
+ ["models.audio", "models"],
7
+ ["models.text", "models"],
8
+ ["doctor.report", "doctor.reports"],
9
+ ["contacts.aliases.rm", "contacts.aliases.remove"],
10
+ ["contacts.alias.rm", "contacts.aliases.remove"],
11
+ ["contacts.alias", "contacts.aliases"],
12
+ ["contacts.profile", "contacts.info"],
13
+ ["contacts.timeline", "contacts.events"],
14
+ ["chats.links.revoke", "chats.links.remove"],
15
+ ["chats.link.revoke", "chats.links.remove"],
16
+ ["chats.link", "chats.links"],
17
+ ["chats.members.audit", "chats.members.check"],
18
+ ["chats.requests.accept", "chats.requests.confirm"],
19
+ ["chats.requests.decline", "chats.requests.reject"],
20
+ ["metadata.get", "metadata.show"],
21
+ ["metadata.refresh", "metadata.sync"],
22
+ ["tasks.plan", "tasks.update"],
23
+ ["tags.auto", "tags.rebuild"],
24
+ ["attachments.text.set", "attachments.update"],
25
+ ["attachments.text", "attachments.update"],
26
+ ["conversations.build", "conversations.links.rebuild"],
27
+ ["conversations.embed", "conversations.embeddings"],
28
+ ["conversations.batches.next", "conversations.batches.show"],
29
+ ["conversations.consents.revoke", "conversations.consents.remove"],
30
+ ["stats.messages.counters.refresh", "stats.messages.counters.sync"],
31
+ ["store.gaps.plan", "store.gaps.show"],
32
+ ["store.reindex", "store.rebuild"],
33
+ ];
34
+ /** Canonical names for internal permission keys; saved moved paths are refused below. */
35
+ export const canonicalPermissionKey = (key) => {
36
+ if (key.startsWith("bot."))
37
+ return `bot.${canonicalPermissionKey(key.slice(4))}`;
38
+ const renamed = RENAMED.find(([old]) => key === old || key.startsWith(`${old}.`));
39
+ return renamed ? renamed[1] + key.slice(renamed[0].length) : key;
40
+ };
41
+ /** WC-PERM-8: a moved saved path must name the replacement before any command runs. */
42
+ export const canonicalPermissions = (levels) => {
43
+ for (const key of Object.keys(levels)) {
44
+ const canonical = canonicalPermissionKey(key);
45
+ if (key !== canonical)
46
+ throw new CliError("configuration_error", `permission path moved — rename permissions.${key} to permissions.${canonical}`, { permission: key, replacement: canonical, retryable: false });
47
+ }
48
+ return { ...levels };
49
+ };
@@ -31,7 +31,7 @@ export declare const isBotApi: (path: readonly string[]) => boolean;
31
31
  */
32
32
  export declare const botApiAllowed: (levels: Readonly<Record<PermissionKey, Level>>) => boolean;
33
33
  /** The resources at the top of the command tree, which `readOnly` and `allow` turn read-only as a whole. */
34
- export declare const RESOURCES: readonly ["messages", "reactions", "polls", "topics", "chats", "contacts", "account", "bot", "conversations", "tags", "mentions", "search", "searches", "tasks", "replies", "attachments", "stats", "store", "metadata", "mcp", "server", "models", "schedule", "doctor", "images"];
34
+ export declare const RESOURCES: readonly ["messages", "drafts", "reactions", "polls", "topics", "chats", "contacts", "account", "bot", "conversations", "tags", "mentions", "search", "searches", "tasks", "replies", "attachments", "stats", "store", "metadata", "mcp", "server", "models", "schedule", "doctor", "images"];
35
35
  /** The command path an old `allow` word stood for, as one key — what an MCP tool's `permission` is checked against. */
36
36
  export declare const keyOfWord: (word: Permission) => PermissionKey;
37
37
  /**
@@ -1,4 +1,5 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
+ import { canonicalPermissionKey, canonicalPermissions } from "./permission-aliases.js";
2
3
  /**
3
4
  * What a profile may be allowed to do (max-cli `CLI-37`, `NEED-251`). One name per thing the owner would
4
5
  * recognise, so `sessions` — which logs the owner out of their phone — never rides along with
@@ -91,12 +92,11 @@ export const DEFAULT_PERMISSIONS = {
91
92
  "server.start": "ask",
92
93
  "server.stop": "ask",
93
94
  "server.restart": "ask",
94
- "models.audio.download": "ask",
95
- "models.text.download": "ask",
95
+ "models.download": "ask",
96
96
  "images.models.download": "ask",
97
97
  "schedule.run": "ask",
98
98
  "schedule.exec": "ask",
99
- "doctor.report.create": "ask",
99
+ "doctor.reports.create": "ask",
100
100
  };
101
101
  /** Commands under a housekeeping word, or store maintenance, that still have a key of their own: their path. */
102
102
  const KEYED_UNDER_HOUSEKEEPING = new Set(Object.keys(DEFAULT_PERMISSIONS).filter((key) => /^(store|server|models|images|schedule|doctor)\./.test(key)));
@@ -122,6 +122,7 @@ export const botApiAllowed = (levels) => {
122
122
  /** The resources at the top of the command tree, which `readOnly` and `allow` turn read-only as a whole. */
123
123
  export const RESOURCES = [
124
124
  "messages",
125
+ "drafts",
125
126
  "reactions",
126
127
  "polls",
127
128
  "topics",
@@ -162,7 +163,7 @@ const OLD_WORDS = {
162
163
  "chats.leave",
163
164
  "chats.members",
164
165
  "chats.admins",
165
- "chats.link",
166
+ "chats.links",
166
167
  "chats.requests",
167
168
  "chats.moderate",
168
169
  "topics.enable",
@@ -198,7 +199,7 @@ export const layerPermissions = (layers) => {
198
199
  const sources = {};
199
200
  for (const [from, layer] of layers) {
200
201
  const nearer = Object.keys(levels);
201
- for (const [key, level] of Object.entries(layer ?? {})) {
202
+ for (const [key, level] of Object.entries(canonicalPermissions(layer ?? {}))) {
202
203
  if (nearer.some((named) => key === named || key.startsWith(`${named}.`)))
203
204
  continue;
204
205
  levels[key] = level;
@@ -235,6 +236,8 @@ const FORMER_KEYS = { "chats.unmute": "chats.mute" };
235
236
  * A path nothing names is allowed.
236
237
  */
237
238
  export const levelFor = (permissions, key, defaults = DEFAULT_PERMISSIONS) => {
239
+ permissions = canonicalPermissions(permissions);
240
+ key = canonicalPermissionKey(key);
238
241
  const found = profileLevel(permissions, key, defaults);
239
242
  const ceiling = permissions[CEILING_KEY];
240
243
  return ceiling && ORDER[ceiling] < ORDER[found.level] ? { level: ceiling, key: CEILING_KEY } : found;
@@ -289,7 +292,7 @@ const KIND_KEYS = {
289
292
  /** The command path a guarded write belongs to, when the caller did not name it. */
290
293
  export const keyForWrite = (kind, action) => {
291
294
  if (kind === "chat")
292
- return CHAT_KEYS[action ?? ""] ?? `chats.${action ?? "update"}`;
295
+ return canonicalPermissionKey(CHAT_KEYS[action ?? ""] ?? `chats.${action ?? "update"}`);
293
296
  if (kind === "account") {
294
297
  const key = ACCOUNT_KEYS[action];
295
298
  if (!key)
@@ -300,6 +303,7 @@ export const keyForWrite = (kind, action) => {
300
303
  };
301
304
  /** `keyForWrite` read backwards, as far as a command's key says it: an unpin is a `pin`, a chat change names its action. */
302
305
  export const writeOfKey = (key) => {
306
+ key = canonicalPermissionKey(key);
303
307
  const account = Object.entries(ACCOUNT_KEYS).find(([, named]) => named === key)?.[0];
304
308
  if (account)
305
309
  return { kind: "account", action: account };
@@ -316,20 +320,21 @@ export const writeOfKey = (key) => {
316
320
  };
317
321
  /** Write keys written out where a write is checked; `permission-keys.test.ts` keeps this list whole. */
318
322
  const NAMED_WRITE_KEYS = [
323
+ "drafts",
319
324
  "store.gaps.repair",
320
325
  "messages.sync-first",
321
326
  "messages.server-search",
322
327
  "messages.press",
323
328
  "chats.start",
324
329
  "chats.app",
325
- "stats.messages.counters.refresh",
330
+ "stats.messages.counters.sync",
326
331
  "stats.messages.show.sync-first",
327
332
  "stats.messages.top.sync-first",
328
333
  "stats.contacts.top.sync-first",
329
334
  "account.sessions.list",
330
335
  "attachments.extract",
331
336
  "attachments.move",
332
- "attachments.text.set",
337
+ "attachments.update",
333
338
  "bot.callbacks.answer",
334
339
  "bot.chats.action",
335
340
  "bot.chats.admins.add",
@@ -344,14 +349,14 @@ const NAMED_WRITE_KEYS = [
344
349
  "bot.messages.unpin",
345
350
  "chats.admins.add",
346
351
  "chats.admins.remove",
347
- "chats.link.create",
348
- "chats.link.reset",
349
- "chats.link.revoke",
350
- "chats.link.update",
352
+ "chats.links.create",
353
+ "chats.links.reset",
354
+ "chats.links.remove",
355
+ "chats.links.update",
351
356
  "chats.members.add",
352
357
  "chats.members.remove",
353
358
  "chats.moderate",
354
- "conversations.embed",
359
+ "conversations.embeddings",
355
360
  "conversations.links",
356
361
  "polls.close",
357
362
  "polls.create",
@@ -359,13 +364,13 @@ const NAMED_WRITE_KEYS = [
359
364
  "searches.clear",
360
365
  "searches.create",
361
366
  "searches.delete",
362
- "contacts.alias.set",
363
- "contacts.alias.rm",
367
+ "contacts.aliases.set",
368
+ "contacts.aliases.remove",
364
369
  "contacts.notes.add",
365
370
  "contacts.notes.edit",
366
371
  "contacts.notes.remove",
367
- "tags.auto",
368
- "metadata.refresh",
372
+ "tags.rebuild",
373
+ "metadata.sync",
369
374
  "mentions.rebuild",
370
375
  "mentions.confirm",
371
376
  "mentions.reject",
@@ -378,7 +383,7 @@ const NAMED_WRITE_KEYS = [
378
383
  "tasks.duplicates.reject",
379
384
  "tasks.link",
380
385
  "tasks.merge",
381
- "tasks.plan",
386
+ "tasks.update",
382
387
  "tasks.release",
383
388
  "tasks.scan",
384
389
  "tasks.unmerge",
@@ -395,7 +400,7 @@ export const WRITE_KEYS = [
395
400
  ...Object.values(ACCOUNT_KEYS),
396
401
  ...Object.values(CHAT_KEYS),
397
402
  ...PERMISSIONS.flatMap((word) => [...OLD_WORDS[word], ...botKeysOf(word)]),
398
- ]),
403
+ ].map(canonicalPermissionKey)),
399
404
  ];
400
405
  /** Commands that look after the tool and its files and never show a message: no level stops them. */
401
406
  const HOUSEKEEPING = new Set([
@@ -481,6 +486,9 @@ export const readKeysForCommand = (path) => {
481
486
  * `undefined` for a path this package does not know — a CLI's own command, which its CLI maps.
482
487
  */
483
488
  export const keyForCommand = (path) => {
489
+ if (path.length === 0)
490
+ return null;
491
+ path = canonicalPermissionKey(path.join(".")).split(".");
484
492
  const [top, next] = path;
485
493
  if (top === "bot") {
486
494
  if (next === undefined || BOT_HOUSEKEEPING.has(next))
@@ -503,11 +511,12 @@ export const keyForCommand = (path) => {
503
511
  return null;
504
512
  // The agent's answers write only to the local store: their own key, so read-only messages can still link (phase 4 A10).
505
513
  if (top === "conversations" && next === "links")
506
- return "conversations.links";
514
+ return path[2] === "rebuild" ? "conversations.links.rebuild" : "conversations.links";
507
515
  // Vectors are written to the local store only, and the text never leaves the machine (phase 5 E10).
508
- if (((top === "conversations" || top === "images") && next === "embed") ||
516
+ if ((top === "conversations" && next === "embeddings") ||
517
+ (top === "images" && next === "embed") ||
509
518
  (top === "conversations" && next === "rebuild"))
510
- return "conversations.embed";
519
+ return "conversations.embeddings";
511
520
  // Which files a message has, and where they are saved, tells as much as a message does; so does who
512
521
  // shared which document link, and when.
513
522
  if ((top === "attachments" && next === "list") || (top === "chats" && next === "documents"))
@@ -519,8 +528,7 @@ export const keyForCommand = (path) => {
519
528
  return path.join(".");
520
529
  if (top === "messages" && next === "sync")
521
530
  return "messages";
522
- if (SHOW_MESSAGES.has(top) ||
523
- (top === "contacts" && (next === "context" || next === "prepare" || next === "timeline")))
531
+ if (SHOW_MESSAGES.has(top) || (top === "contacts" && (next === "context" || next === "prepare" || next === "events")))
524
532
  return "messages";
525
533
  if (RESOURCES.includes(top))
526
534
  return path.join(".");
@@ -536,10 +544,12 @@ export const permissionOverrides = (entries = []) => {
536
544
  throw new CliError("validation_error", "--permission takes a command path and level, e.g. messages.send=allow (deny, readonly, ask, allow)");
537
545
  levels[key] = level;
538
546
  }
539
- return levels;
547
+ return canonicalPermissions(levels);
540
548
  };
541
549
  /** No startup flag grants access: an override may narrow the profile's level, never lift it or a key under it. */
542
550
  export const assertOverridesTighten = (overrides, profile) => {
551
+ overrides = canonicalPermissions(overrides);
552
+ profile = canonicalPermissions(profile);
543
553
  for (const [key, level] of Object.entries(overrides)) {
544
554
  const under = Object.keys(profile).filter((named) => named.startsWith(`${key}.`));
545
555
  const lifted = [key, ...under].find((named) => ORDER[level] > ORDER[levelFor(profile, named).level]);
@@ -111,14 +111,14 @@ export const adminService = (deps) => {
111
111
  return { operationId, chat: card };
112
112
  },
113
113
  link: async (chat) => {
114
- const group = await capability(await online("chats link show"), "group", "read a group")(chat);
114
+ const group = await capability(await online("chats links show"), "group", "read a group")(chat);
115
115
  if (group.link === null) {
116
116
  throw new CliError("not_found", `${group.title ?? group.id} shows you no invite link — only admins, or members when the group allows it`);
117
117
  }
118
118
  return { chatId: group.id, title: group.title, link: group.link };
119
119
  },
120
120
  resetLink: async (chat) => {
121
- const connection = await online("chats link reset");
121
+ const connection = await online("chats links reset");
122
122
  const reset = capability(connection, "resetInviteLink", "replace an invite link");
123
123
  const { id: chatId } = await connection.resolve(chat);
124
124
  const operationId = newOperationId();
@@ -127,7 +127,7 @@ export const adminService = (deps) => {
127
127
  },
128
128
  createLink: async (chat, { approval, expires, maxUses }) => {
129
129
  const expiresAt = linkLimits({ approval, expires, maxUses });
130
- const connection = await online("chats link create");
130
+ const connection = await online("chats links create");
131
131
  const create = capability(connection, "createInviteLink", "make another invite link");
132
132
  const { id: chatId } = await connection.resolve(chat);
133
133
  const operationId = newOperationId();
@@ -193,8 +193,8 @@ export const adminService = (deps) => {
193
193
  },
194
194
  revokeLink: async (chat, link) => {
195
195
  if (link.trim() === "")
196
- throw new CliError("validation_error", "which link? give it as `chats link list` shows it");
197
- const connection = await online("chats link revoke");
196
+ throw new CliError("validation_error", "which link? give it as `chats links list` shows it");
197
+ const connection = await online("chats links remove");
198
198
  const revoke = capability(connection, "revokeInviteLink", "revoke an invite link");
199
199
  const { id: chatId } = await connection.resolve(chat);
200
200
  const operationId = newOperationId();
@@ -203,12 +203,12 @@ export const adminService = (deps) => {
203
203
  },
204
204
  updateLink: async (chat, link, { approval, expires, maxUses }) => {
205
205
  if (link.trim() === "")
206
- throw new CliError("validation_error", "which link? give it as `chats link list` shows it");
206
+ throw new CliError("validation_error", "which link? give it as `chats links list` shows it");
207
207
  if (approval === undefined && expires === undefined && maxUses === undefined)
208
208
  throw new CliError("validation_error", "nothing to change: give --approval, --no-approval, --expire-time or --max-uses");
209
209
  const limited = linkLimits({ approval, expires: expires === "never" ? undefined : expires, maxUses });
210
210
  const expiresAt = expires === "never" ? null : limited;
211
- const connection = await online("chats link update");
211
+ const connection = await online("chats links update");
212
212
  const update = capability(connection, "updateInviteLink", "change an invite link");
213
213
  const { id: chatId } = await connection.resolve(chat);
214
214
  const change = {
@@ -161,7 +161,7 @@ export const chatsService = (deps) => ({
161
161
  },
162
162
  audit: async (chat, { budget = AUDIT_BUDGET, minScore = AUDIT_MIN_SCORE, pauseMs = AUDIT_PAUSE_MS, deep = 0, registry }) => {
163
163
  if (deps.offline) {
164
- throw new CliError("validation_error", "`chats members audit` reads the member list from the messenger; not with --offline");
164
+ throw new CliError("validation_error", "`chats members check` reads the member list from the messenger; not with --offline");
165
165
  }
166
166
  const connection = await deps.connection();
167
167
  const { chatId: id, members: read, more, participants } = await readMembers(connection, chat, budget, pauseMs);
@@ -61,7 +61,7 @@ export interface ConversationsService {
61
61
  batchStatus(chat: string, size: number): Promise<BatchStatus>;
62
62
  /** The next window for the agent; `undefined` when every message is answered (A1–A4). */
63
63
  nextBatch(chat: string, size: number): Promise<LinkBatch | undefined>;
64
- /** Stores the agent's answer to a batch, all or nothing (A5); `conversations build` then uses it. */
64
+ /** Stores the agent's answer to a batch, all or nothing (A5); `conversations links rebuild` then uses it. */
65
65
  addAnswers(batch: string, answer: AgentAnswer): Promise<{
66
66
  chat: Id;
67
67
  stored: number;
@@ -39,7 +39,7 @@ export const conversationsService = (deps) => {
39
39
  const store = await accountStore(deps);
40
40
  return { store, chatId: await chatIdIn(deps.messenger, chat, store, { includeLeft: true }) };
41
41
  };
42
- const notBuilt = (chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${typedCommand(deps.messenger.app.command, `conversations build --chat ${chatId}`)}\``);
42
+ const notBuilt = (chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${typedCommand(deps.messenger.app.command, `conversations links rebuild --chat ${chatId}`)}\``);
43
43
  return {
44
44
  build: async (chat, { maxMessages, check } = {}) => {
45
45
  check?.();
@@ -115,7 +115,7 @@ export const countersService = (deps) => {
115
115
  return preview;
116
116
  if (deps.offline)
117
117
  throw new CliError("validation_error", "counter refresh requires an online session");
118
- const request = { chatId: null, key: "stats.messages.counters.refresh" };
118
+ const request = { chatId: null, key: "stats.messages.counters.sync" };
119
119
  await deps.guard.ask?.(request);
120
120
  deps.guard.check(request, { reserve: false });
121
121
  const controller = new AbortController(), timer = setTimeout(() => controller.abort(new Error("counter refresh time budget exhausted")), time);
@@ -17,11 +17,11 @@ const PER_SESSION = 8;
17
17
  const PER_REQUEST = 256;
18
18
  /** e5's tokenizer reads ~3.7 characters a token in Russian; 3 keeps the bound above the real count. */
19
19
  const CHARS_PER_TOKEN_AT_LEAST = 3;
20
- const notDownloaded = (deps, id) => new CliError("not_found", `${id} is not downloaded — \`${typedCommand(deps.messenger.app.command, `models text download ${id}`)}\``, {
20
+ const notDownloaded = (deps, id) => new CliError("not_found", `${id} is not downloaded — \`${typedCommand(deps.messenger.app.command, `models download ${id}`)}\``, {
21
21
  reason: "model.not-downloaded",
22
22
  model: id,
23
23
  });
24
- export const notBuiltError = (deps, chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${typedCommand(deps.messenger.app.command, `conversations build --chat ${chatId}`)}\``);
24
+ export const notBuiltError = (deps, chatId) => new CliError("not_found", `chat ${chatId} has no conversations yet — \`${typedCommand(deps.messenger.app.command, `conversations links rebuild --chat ${chatId}`)}\``);
25
25
  export const embeddingTarget = (deps, choice = DEFAULT_TEXT_MODEL) => {
26
26
  if (typeof choice !== "string") {
27
27
  const { remote, apiKey, concurrency = 4 } = choice;