@wirecat/cli-core 0.400.0 → 0.402.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 (78) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +1 -1
  3. package/dist/cli/bot/api.js +5 -2
  4. package/dist/cli/bot/store.js +1 -1
  5. package/dist/cli/config-command.js +13 -7
  6. package/dist/cli/context.d.ts +2 -0
  7. package/dist/cli/index.d.ts +2 -1
  8. package/dist/cli/index.js +2 -1
  9. package/dist/cli/meeting-evidence-answer.d.ts +46 -0
  10. package/dist/cli/meeting-evidence-answer.js +22 -0
  11. package/dist/cli/meeting-preparation-answer.js +1 -13
  12. package/dist/cli/messenger/account-sessions-command.js +0 -2
  13. package/dist/cli/messenger/archive-commands.js +1 -1
  14. package/dist/cli/messenger/doctor-command.js +0 -1
  15. package/dist/cli/messenger/message-writes.d.ts +192 -2
  16. package/dist/cli/messenger/message-writes.js +6 -2
  17. package/dist/cli/messenger/messages-send-command.js +9 -6
  18. package/dist/cli/messenger/models-command.js +0 -3
  19. package/dist/cli/messenger/records-command.js +0 -2
  20. package/dist/cli/messenger/review.js +1 -1
  21. package/dist/cli/messenger/schedule-command.js +2 -2
  22. package/dist/cli/messenger/search-answers.js +2 -0
  23. package/dist/cli/messenger/server-command.js +0 -3
  24. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  25. package/dist/cli/messenger/store-maintenance-command.js +7 -7
  26. package/dist/cli/messenger/stored.js +4 -1
  27. package/dist/cli/messenger/tasks-command.js +10 -6
  28. package/dist/cli/program.js +5 -1
  29. package/dist/documents/index.d.ts +1 -0
  30. package/dist/documents/index.js +1 -0
  31. package/dist/entities/hit.d.ts +4 -0
  32. package/dist/entities/hit.js +4 -0
  33. package/dist/entities/index.d.ts +285 -0
  34. package/dist/entities/index.js +5 -1
  35. package/dist/entities/meeting-evidence.d.ts +86 -0
  36. package/dist/entities/meeting-evidence.js +25 -0
  37. package/dist/entities/meeting-preparation-parts.d.ts +14 -0
  38. package/dist/entities/meeting-preparation-parts.js +15 -13
  39. package/dist/entities/meeting.d.ts +1 -1
  40. package/dist/entities/meeting.js +1 -1
  41. package/dist/entities/message-write.d.ts +187 -0
  42. package/dist/entities/message-write.js +2 -0
  43. package/dist/errors/reasons.js +2 -0
  44. package/dist/mcp/agent.js +12 -6
  45. package/dist/meetings/cli/index.js +3 -1
  46. package/dist/meetings/index.d.ts +1 -1
  47. package/dist/meetings/index.js +1 -1
  48. package/dist/meetings/mcp/index.d.ts +15 -15
  49. package/dist/meetings/read-answers.d.ts +18 -0
  50. package/dist/meetings/read-answers.js +30 -0
  51. package/dist/ports/messenger.d.ts +10 -1
  52. package/dist/search/dictionary.d.ts +20 -0
  53. package/dist/search/dictionary.js +106 -0
  54. package/dist/sends/guard.d.ts +1 -1
  55. package/dist/sends/guard.js +9 -5
  56. package/dist/sends/journal.d.ts +1 -1
  57. package/dist/sends/permissions.d.ts +10 -1
  58. package/dist/sends/permissions.js +43 -1
  59. package/dist/sends/upload.d.ts +4 -4
  60. package/dist/sends/upload.js +24 -20
  61. package/dist/services/documents/mirrors.d.ts +98 -0
  62. package/dist/services/documents/mirrors.js +144 -0
  63. package/dist/services/messages.js +1 -0
  64. package/dist/services/search-all-meetings.js +21 -8
  65. package/dist/services/task-rules.d.ts +1 -1
  66. package/dist/services/tasks.d.ts +2 -2
  67. package/dist/services/tasks.js +1 -1
  68. package/dist/store/sqlite/drizzle/bun.js +3 -3
  69. package/dist/store/sqlite/drizzle/bun.js.map +1 -1
  70. package/dist/store/sqlite/drizzle/core.js +2 -2
  71. package/dist/store/sqlite/drizzle/core.js.map +1 -1
  72. package/dist/store/sqlite/drizzle/node.js +3 -3
  73. package/dist/store/sqlite/drizzle/node.js.map +1 -1
  74. package/dist/store/sqlite/drizzle/{shared-O7CTKP45.js → shared-HKXF6HKX.js} +57 -57
  75. package/dist/store/sqlite/drizzle/{shared-O7CTKP45.js.map → shared-HKXF6HKX.js.map} +1 -1
  76. package/dist/store/sqlite/transcript-query.js +15 -5
  77. package/package.json +4 -2
  78. package/parity.json +6 -3
@@ -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;
@@ -1,5 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import type { MeetingLink } from "./links.js";
3
+ import type { Event } from "./model.js";
3
4
  import type { MeetingReviewItem } from "./review.js";
4
5
  import type { MeetingSpeakerStats, MeetingStats } from "./stats.js";
5
6
  /** One meeting in `review`: what the store holds about it, never transcript text. */
@@ -165,3 +166,20 @@ export declare const meetingPassage: v.StrictObjectSchema<{
165
166
  readonly revision: v.PicklistSchema<["current", "superseded"], undefined>;
166
167
  }, undefined>, undefined>;
167
168
  }, undefined>;
169
+ /** A stored event a meeting belongs to; `id` is what `meetings update --event-id` takes. */
170
+ export declare const event: v.StrictObjectSchema<{
171
+ readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
172
+ readonly event_series_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
173
+ readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
174
+ readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
175
+ readonly location: v.NullableSchema<v.StringSchema<undefined>, undefined>;
176
+ readonly starts_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
177
+ readonly ends_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
178
+ readonly timezone: v.NullableSchema<v.StringSchema<undefined>, undefined>;
179
+ /** `auto`: made from a meeting; `owner`: made by hand. */
180
+ readonly origin: v.PicklistSchema<["auto", "owner"], undefined>;
181
+ readonly deleted_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
182
+ readonly created_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
183
+ readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
184
+ }, undefined>;
185
+ export declare const eventItem: (one: Event) => v.InferOutput<typeof event>;
@@ -181,3 +181,33 @@ export const meetingPassage = v.strictObject({
181
181
  revision,
182
182
  })),
183
183
  });
184
+ /** A stored event a meeting belongs to; `id` is what `meetings update --event-id` takes. */
185
+ export const event = v.strictObject({
186
+ id: v.pipe(v.string(), v.minLength(1)),
187
+ event_series_id: v.nullable(text),
188
+ title: v.nullable(text),
189
+ description: v.nullable(text),
190
+ location: v.nullable(text),
191
+ starts_at: v.nullable(isoTime),
192
+ ends_at: v.nullable(isoTime),
193
+ timezone: v.nullable(text),
194
+ /** `auto`: made from a meeting; `owner`: made by hand. */
195
+ origin: v.picklist(["auto", "owner"]),
196
+ deleted_at: v.nullable(isoTime),
197
+ created_at: isoTime,
198
+ updated_at: isoTime,
199
+ });
200
+ export const eventItem = (one) => ({
201
+ id: String(one.id),
202
+ event_series_id: one.eventSeriesId === null ? null : String(one.eventSeriesId),
203
+ title: one.title,
204
+ description: one.description,
205
+ location: one.location,
206
+ starts_at: iso(one.startsAt),
207
+ ends_at: iso(one.endsAt),
208
+ timezone: one.timezone,
209
+ origin: one.origin,
210
+ deleted_at: iso(one.deletedAt),
211
+ created_at: new Date(one.createdAt).toISOString(),
212
+ updated_at: new Date(one.updatedAt).toISOString(),
213
+ });
@@ -41,7 +41,10 @@ export interface SendOptions {
41
41
  formatting?: TextSpan[];
42
42
  /** ISO time: the messenger holds it and sends it then, under a new id. */
43
43
  at?: string;
44
- /** Sent in one message, `text` as the caption. More than one is an album. */
44
+ /**
45
+ * Sent in one message, `text` as the caption. More than one is an album: photos first, then files, each in the
46
+ * order given. An adapter whose messenger cannot send one refuses it with `validation_error` before anything goes.
47
+ */
45
48
  attachments?: Upload[];
46
49
  /** One sticker by id, alone: no text, no attachment. Only where `Messenger.stickers` is on. */
47
50
  sticker?: Id;
@@ -82,7 +85,13 @@ export interface NewPoll {
82
85
  closeAfter?: number;
83
86
  }
84
87
  export interface Sent {
88
+ /** The message sent; for an album, its first. */
85
89
  message: Message;
90
+ /**
91
+ * For an album, every message it made, in order, `message` among them. Unset for a single message, and where an
92
+ * adapter has not learned to answer with them yet.
93
+ */
94
+ messages?: Message[];
86
95
  /** The send's identity. Repeat it after an unknown outcome, never a new one. */
87
96
  sendId: string;
88
97
  }
@@ -0,0 +1,20 @@
1
+ /** The Hunspell dictionary that tells a real word from a garbled one, for each language the store stems. */
2
+ export declare const DICTIONARIES: {
3
+ readonly russian: "dictionary-ru";
4
+ readonly english: "dictionary-en";
5
+ readonly spanish: "dictionary-es";
6
+ };
7
+ export type DictionaryLanguage = keyof typeof DICTIONARIES;
8
+ /**
9
+ * `known`: whether a folded word the index stores is real in one of `languages` that write in its alphabet.
10
+ * `covers`: whether any of them does; without one, a garbled spelling cannot be told from a real word.
11
+ */
12
+ export declare const knownWord: (languages: readonly DictionaryLanguage[]) => {
13
+ covers: (word: string) => boolean;
14
+ known: (word: string) => boolean;
15
+ };
16
+ /** The dictionaries of the store's stemmers: Russian for Cyrillic, each named Latin stemmer for Latin. */
17
+ export declare const dictionaryLanguages: (stemmers: {
18
+ cyrillic: string;
19
+ latin: string;
20
+ }) => DictionaryLanguage[];
@@ -0,0 +1,106 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fold } from "../store/normalize.js";
3
+ /** The Hunspell dictionary that tells a real word from a garbled one, for each language the store stems. */
4
+ export const DICTIONARIES = { russian: "dictionary-ru", english: "dictionary-en", spanish: "dictionary-es" };
5
+ /** `fold`, without Unicode normalisation for plain Russian letters: it would double the time Russian loads in. */
6
+ const folded = (word) => /^[а-яё'-]*$/.test(word) ? word.replaceAll("ё", "е").replaceAll("й", "и") : fold(word);
7
+ const loaded = new Map();
8
+ /**
9
+ * Hunspell's own check, an affix at a time, instead of nspell's: nspell expands Russian's 146,269 entries into
10
+ * 1.44 million forms (1.4 s, 172 MB) where this loads in about 0.2 s. The index stores words folded as `normalize`
11
+ * folds them, so entries, affixes and conditions are folded too; a condition such as `[^ейоь]` then refuses и as
12
+ * well, which changed none of 12,769 sample words. Continuation classes and compounds are not followed: a word
13
+ * only they make counts as unknown and may widen.
14
+ */
15
+ const dictionary = (language) => {
16
+ const cached = loaded.get(language);
17
+ if (cached)
18
+ return cached;
19
+ const file = (name) => readFileSync(new URL(name, import.meta.resolve(DICTIONARIES[language])), "utf8");
20
+ const words = new Map();
21
+ for (const line of file("index.dic").split("\n").slice(1)) {
22
+ const [entry, flags = ""] = line.trim().split("/");
23
+ if (!entry)
24
+ continue;
25
+ // Names and abbreviations are capitalised in the dictionary; the index is lower case.
26
+ const spelled = entry.toLowerCase();
27
+ const key = folded(spelled);
28
+ words.set(key, (words.get(key) ?? "") + flags);
29
+ }
30
+ const suffixes = new Map();
31
+ const prefixes = new Map();
32
+ const crosses = new Set();
33
+ let longest = 0;
34
+ for (const line of file("index.aff").split("\n")) {
35
+ const [kind, flag, strip, add, condition] = line.trim().split(/\s+/);
36
+ if ((kind !== "SFX" && kind !== "PFX") || flag === undefined || strip === undefined || add === undefined)
37
+ continue;
38
+ if (!condition) {
39
+ if (strip === "Y")
40
+ crosses.add(`${kind}${flag}`);
41
+ continue;
42
+ }
43
+ const affix = folded(add.split("/")[0].replace(/^0$/, ""));
44
+ const pattern = condition === "." ? "" : folded(condition);
45
+ const into = kind === "SFX" ? suffixes : prefixes;
46
+ const rules = into.get(affix) ?? [];
47
+ rules.push({
48
+ flag,
49
+ strip: strip === "0" ? "" : folded(strip),
50
+ condition: new RegExp(kind === "SFX" ? `${pattern}$` : `^${pattern}`),
51
+ cross: crosses.has(`${kind}${flag}`),
52
+ });
53
+ into.set(affix, rules);
54
+ longest = Math.max(longest, affix.length);
55
+ }
56
+ const made = { words, suffixes, prefixes, longest };
57
+ loaded.set(language, made);
58
+ return made;
59
+ };
60
+ const entry = ({ words }, key, rule, also) => {
61
+ const flags = words.get(key) ?? "";
62
+ return flags.includes(rule.flag) && (also === undefined || flags.includes(also)) && rule.condition.test(key);
63
+ };
64
+ /** A suffix on a dictionary word; `prefix` is the flag of a prefix already taken off, which both must allow. */
65
+ const suffixed = (found, word, prefix) => {
66
+ for (let cut = 0; cut <= Math.min(word.length - 1, found.longest); cut++)
67
+ for (const rule of found.suffixes.get(word.slice(word.length - cut)) ?? [])
68
+ if ((prefix === undefined || rule.cross) &&
69
+ entry(found, word.slice(0, word.length - cut) + rule.strip, rule, prefix))
70
+ return true;
71
+ return false;
72
+ };
73
+ const spelled = (language, word) => {
74
+ const found = dictionary(language);
75
+ if (found.words.has(word) || suffixed(found, word))
76
+ return true;
77
+ for (let cut = 1; cut <= Math.min(word.length - 1, found.longest); cut++)
78
+ for (const rule of found.prefixes.get(word.slice(0, cut)) ?? []) {
79
+ const rest = rule.strip + word.slice(cut);
80
+ if (entry(found, rest, rule) || (rule.cross && suffixed(found, rest, rule.flag)))
81
+ return true;
82
+ }
83
+ return false;
84
+ };
85
+ const CYRILLIC = /\p{Script=Cyrillic}/u;
86
+ const SCRIPT_OF = {
87
+ russian: "cyrillic",
88
+ english: "latin",
89
+ spanish: "latin",
90
+ };
91
+ /**
92
+ * `known`: whether a folded word the index stores is real in one of `languages` that write in its alphabet.
93
+ * `covers`: whether any of them does; without one, a garbled spelling cannot be told from a real word.
94
+ */
95
+ export const knownWord = (languages) => {
96
+ const byScript = (word) => {
97
+ const script = CYRILLIC.test(word) ? "cyrillic" : "latin";
98
+ return languages.filter((language) => SCRIPT_OF[language] === script);
99
+ };
100
+ return {
101
+ covers: (word) => byScript(word).length > 0,
102
+ known: (word) => byScript(word).some((language) => spelled(language, word)),
103
+ };
104
+ };
105
+ /** The dictionaries of the store's stemmers: Russian for Cyrillic, each named Latin stemmer for Latin. */
106
+ export const dictionaryLanguages = (stemmers) => [stemmers.cyrillic, ...stemmers.latin.split(",")].filter((name) => Object.hasOwn(DICTIONARIES, name));
@@ -12,7 +12,7 @@ export interface GuardRequest {
12
12
  chatId: Id | null;
13
13
  kind?: SendKind;
14
14
  action?: ChatAction | AccountAction;
15
- /** Messages a deletion names, or people a chat change adds. */
15
+ /** Messages a deletion names or an album sends, or people a chat change adds. */
16
16
  count?: number;
17
17
  sendId?: string;
18
18
  sendAs?: Id;
@@ -40,11 +40,11 @@ const countsTowardLimit = ({ kind = "message", action, notify }) => kind === "me
40
40
  action === "join" ||
41
41
  action === "contact-import";
42
42
  /**
43
- * Each deleted message, each person added to a group or let in by a request, and each imported number
44
- * counts as one.
43
+ * Each deleted message, each photo or file of an album (`NEED-1660`), each person added to a group or let in by a
44
+ * request, and each imported number counts as one.
45
45
  */
46
- const weightOf = ({ kind, action, count, people }) => kind === "delete"
47
- ? (count ?? 1)
46
+ const weightOf = ({ kind = "message", action, count, people }) => kind === "delete" || (kind === "message" && action === undefined)
47
+ ? Math.max(1, count ?? 1)
48
48
  : action === "create" || action === "members.add" || action === "requests.accept" || action === "contact-import"
49
49
  ? Math.max(1, people ?? count ?? 1)
50
50
  : 1;
@@ -180,7 +180,11 @@ export const sendGuard = ({ profile, recipients, command = recipients.command, r
180
180
  reservation: id,
181
181
  ...(kind ? { kind } : {}),
182
182
  ...(action ? { action } : {}),
183
- ...(asked.people === undefined ? {} : kind === "delete" ? { count: asked.people } : { people: asked.people }),
183
+ ...(asked.people === undefined
184
+ ? {}
185
+ : kind === "delete" || kind === "message" || kind === undefined
186
+ ? { count: asked.people }
187
+ : { people: asked.people }),
184
188
  ...(sendId === undefined ? {} : { sendId }),
185
189
  ...(operationId === undefined ? {} : { operationId }),
186
190
  ...(scheduledFor ? { scheduledFor } : {}),
@@ -23,7 +23,7 @@ export interface SendEntry {
23
23
  messageId?: Id;
24
24
  /** The command a write belongs to, where it named one: `polls.create` sets a poll apart from a message. */
25
25
  key?: string;
26
- /** How many messages a `delete` entry named — each one counts toward the hourly limit. */
26
+ /** How many messages a `delete` entry named, or an album sent — each one counts toward the hourly limit. */
27
27
  count?: number;
28
28
  forEveryone?: boolean;
29
29
  /**
@@ -21,8 +21,17 @@ export declare const DEFAULT_PERMISSIONS: Readonly<Record<PermissionKey, Level>>
21
21
  export declare const AUTHORITY_KEY: PermissionKey;
22
22
  /** Whether these levels let an agent run a command that changes what a later run may do; `ask` does, as for any write. */
23
23
  export declare const authorityAllowed: (levels: Readonly<Record<PermissionKey, Level>>) => boolean;
24
+ /** Lets an MCP server offer `bot api`, whose calls skip the send guard and the bot's recipient list (NEED-1679). */
25
+ export declare const BOT_API_KEY: PermissionKey;
26
+ export declare const isBotApi: (path: readonly string[]) => boolean;
27
+ /**
28
+ * Deny by default for an MCP server only: the key stays out of `DEFAULT_PERMISSIONS` because the command line's
29
+ * `bot api` is checked per operation (`bot.api.<operation>`), and a default there would deny it for the owner too.
30
+ * A broader `bot: allow` does not lift it; `ask` goes ahead over MCP, as for any write.
31
+ */
32
+ export declare const botApiAllowed: (levels: Readonly<Record<PermissionKey, Level>>) => boolean;
24
33
  /** The resources at the top of the command tree, which `readOnly` and `allow` turn read-only as a whole. */
25
- 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"];
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"];
26
35
  /** The command path an old `allow` word stood for, as one key — what an MCP tool's `permission` is checked against. */
27
36
  export declare const keyOfWord: (word: Permission) => PermissionKey;
28
37
  /**
@@ -79,7 +79,27 @@ export const DEFAULT_PERMISSIONS = {
79
79
  "replies.send": "deny",
80
80
  // NEED-1662: an agent that changes settings, schedules or recipients could lift its own limits; the owner opts in.
81
81
  "mcp.authority": "deny",
82
+ // NEED-1663: writes no level stopped before, so MCP could not offer them; over MCP `ask` goes ahead.
83
+ "store.backup": "ask",
84
+ "store.restore": "ask",
85
+ "store.migrate": "ask",
86
+ "store.repair": "ask",
87
+ "store.reset": "ask",
88
+ "store.clear": "ask",
89
+ "store.decrypt": "ask",
90
+ "store.copies.delete": "ask",
91
+ "server.start": "ask",
92
+ "server.stop": "ask",
93
+ "server.restart": "ask",
94
+ "models.audio.download": "ask",
95
+ "models.text.download": "ask",
96
+ "images.models.download": "ask",
97
+ "schedule.run": "ask",
98
+ "schedule.exec": "ask",
99
+ "doctor.report.create": "ask",
82
100
  };
101
+ /** Commands under a housekeeping word, or store maintenance, that still have a key of their own: their path. */
102
+ const KEYED_UNDER_HOUSEKEEPING = new Set(Object.keys(DEFAULT_PERMISSIONS).filter((key) => /^(store|server|models|images|schedule|doctor)\./.test(key)));
83
103
  /** Lets an MCP server offer the commands that change what a later run may do (`grantsAuthority`). */
84
104
  export const AUTHORITY_KEY = "mcp.authority";
85
105
  /** Whether these levels let an agent run a command that changes what a later run may do; `ask` does, as for any write. */
@@ -87,6 +107,18 @@ export const authorityAllowed = (levels) => {
87
107
  const { level } = levelFor(levels, AUTHORITY_KEY);
88
108
  return level === "allow" || level === "ask";
89
109
  };
110
+ /** Lets an MCP server offer `bot api`, whose calls skip the send guard and the bot's recipient list (NEED-1679). */
111
+ export const BOT_API_KEY = "bot.api";
112
+ export const isBotApi = (path) => path[0] === "bot" && path[1] === "api";
113
+ /**
114
+ * Deny by default for an MCP server only: the key stays out of `DEFAULT_PERMISSIONS` because the command line's
115
+ * `bot api` is checked per operation (`bot.api.<operation>`), and a default there would deny it for the owner too.
116
+ * A broader `bot: allow` does not lift it; `ask` goes ahead over MCP, as for any write.
117
+ */
118
+ export const botApiAllowed = (levels) => {
119
+ const { level } = levelFor(levels, BOT_API_KEY, { ...DEFAULT_PERMISSIONS, [BOT_API_KEY]: "deny" });
120
+ return level === "allow" || level === "ask";
121
+ };
90
122
  /** The resources at the top of the command tree, which `readOnly` and `allow` turn read-only as a whole. */
91
123
  export const RESOURCES = [
92
124
  "messages",
@@ -109,6 +141,11 @@ export const RESOURCES = [
109
141
  "store",
110
142
  "metadata",
111
143
  "mcp",
144
+ "server",
145
+ "models",
146
+ "schedule",
147
+ "doctor",
148
+ "images",
112
149
  ];
113
150
  const OLD_WORDS = {
114
151
  send: ["messages.send", "polls.create", "chats.start"],
@@ -375,7 +412,7 @@ const HOUSEKEEPING = new Set([
375
412
  ]);
376
413
  /** Under `bot`: its token, its names, its lists and its MCP server. */
377
414
  const BOT_HOUSEKEEPING = new Set(["auth", "list", "sends", "recipients", "mcp"]);
378
- const STORE_MAINTENANCE = new Set(["info", "check", "migrate", "backup", "restore", "repair", "copies"]);
415
+ const STORE_MAINTENANCE = new Set(["info", "check", "copies"]);
379
416
  /** Commands outside `messages` that print what people wrote, so `deny messages` reaches them too. */
380
417
  const SHOW_MESSAGES = new Set(["inbox", "review", "watch", "serve", "store", "conversations"]);
381
418
  /** Before every search moved under `search` (2026-10-09). */
@@ -405,6 +442,9 @@ export const readKeysForCommand = (path) => {
405
442
  return [key, "messages"];
406
443
  if (path[0] === "store" && path[1] === "gaps" && path[2] === "repair")
407
444
  return [key, "messages"];
445
+ // Checked as `messages` before they had keys of their own: `deny messages` keeps stopping them.
446
+ if (path[0] === "store" && ["reset", "clear", "decrypt"].includes(path[1] ?? ""))
447
+ return [key, "messages"];
408
448
  // A search shows what people wrote, so `deny messages` reaches every leaf that reads messages.
409
449
  if (path[0] === "search") {
410
450
  if (path[1] === "notes")
@@ -438,6 +478,8 @@ export const keyForCommand = (path) => {
438
478
  const inner = keyForCommand(path.slice(1));
439
479
  return `bot.${inner ?? path.slice(1).join(".")}`;
440
480
  }
481
+ if (KEYED_UNDER_HOUSEKEEPING.has(path.join(".")))
482
+ return path.join(".");
441
483
  if (top === undefined || HOUSEKEEPING.has(top) || (top === "search" && next === "runs"))
442
484
  return null;
443
485
  // Picture models moved from `models image` (0.270.0); a model is housekeeping wherever it is listed.
@@ -18,12 +18,12 @@ export declare const readUpload: (kind: UploadKind, path: string, { app, env, an
18
18
  photoTypes?: readonly string[];
19
19
  }) => Promise<Upload>;
20
20
  /**
21
- * What a send attaches, as `messages send` and its tool take it. A voice message goes alone, as the
22
- * messengers' own apps send one.
21
+ * What a send attaches, as `messages send` and its tool take it. More than one photo or file is an album: photos
22
+ * first, then files, each in the order given. A voice message goes alone, as the messengers' own apps send one.
23
23
  */
24
24
  export declare const readAttachments: ({ photo, file, voice, asFile, filename, text, }: {
25
- photo?: string;
26
- file?: string;
25
+ photo?: string | readonly string[];
26
+ file?: string | readonly string[];
27
27
  voice?: string;
28
28
  asFile?: boolean;
29
29
  filename?: string;
@@ -197,37 +197,41 @@ export const readUpload = async (kind, path, { app, env = process.env, anyFile =
197
197
  }
198
198
  return { kind, name: basename(path), source: { kind: "file", path: resolve(path), size } };
199
199
  };
200
- const checkFilename = (filename, file) => {
201
- if (file === undefined)
200
+ const checkFilename = (filename, files) => {
201
+ if (files.length === 0)
202
202
  throw new CliError("validation_error", "--filename names a --file; give one");
203
+ if (files.length > 1)
204
+ throw new CliError("validation_error", "--filename names one --file; an album keeps its names");
203
205
  if (filename.trim() === "" || /[/\\\0]/.test(filename) || filename.length > 255)
204
206
  throw new CliError("validation_error", "--filename is a name, not a path: no / or \\, at most 255 characters");
205
207
  };
208
+ const listOf = (value) => value === undefined ? [] : typeof value === "string" ? [value] : value;
206
209
  /**
207
- * What a send attaches, as `messages send` and its tool take it. A voice message goes alone, as the
208
- * messengers' own apps send one.
210
+ * What a send attaches, as `messages send` and its tool take it. More than one photo or file is an album: photos
211
+ * first, then files, each in the order given. A voice message goes alone, as the messengers' own apps send one.
209
212
  */
210
213
  export const readAttachments = async ({ photo, file, voice, asFile, filename, text, }, read) => {
211
- if (voice !== undefined && (photo !== undefined || file !== undefined || (text ?? "").trim() !== "")) {
214
+ const photos = listOf(photo);
215
+ const files = listOf(file);
216
+ if (voice !== undefined && (photos.length > 0 || files.length > 0 || (text ?? "").trim() !== "")) {
212
217
  throw new CliError("validation_error", "a voice message goes alone — no text, no file, no photo beside it");
213
218
  }
214
- if (asFile && file === undefined)
219
+ if (asFile && files.length === 0)
215
220
  throw new CliError("validation_error", "--as-file is about a --file; give one");
216
221
  if (filename !== undefined)
217
- checkFilename(filename, file);
218
- return [
219
- ...(photo === undefined ? [] : [await readUpload("photo", photo, read)]),
220
- ...(file === undefined
221
- ? []
222
- : [
223
- {
224
- ...(await readUpload("file", file, read)),
225
- ...(filename === undefined ? {} : { name: filename }),
226
- ...(asFile ? { asFile: true } : {}),
227
- },
228
- ]),
229
- ...(voice === undefined ? [] : [await readUpload("voice", voice, read)]),
230
- ];
222
+ checkFilename(filename, files);
223
+ const uploads = [];
224
+ for (const one of photos)
225
+ uploads.push(await readUpload("photo", one, read));
226
+ for (const one of files)
227
+ uploads.push({
228
+ ...(await readUpload("file", one, read)),
229
+ ...(filename === undefined ? {} : { name: filename }),
230
+ ...(asFile ? { asFile: true } : {}),
231
+ });
232
+ if (voice !== undefined)
233
+ uploads.push(await readUpload("voice", voice, read));
234
+ return uploads;
231
235
  };
232
236
  /** A sticker to send from a file: a WebP image of at most `maxBytes`, behind the same file guard as `--file`. */
233
237
  export const readStickerFile = async (path, maxBytes, read) => {