@wirecat/cli-core 0.401.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.402.0 — 24.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **MCP offers `bot api`, behind `bot.api`.** The generated `bot api <operation>` commands (raw calls to Bot API methods, which skip the send guard and the bot's recipient list) are bot MCP tools again, offered only where the profile sets `permissions.bot.api` to `allow`; it is `deny` by default for an MCP server, so nothing changes until the owner turns it on, and a broader `bot: allow` does not lift it. A call made after the owner turned it off is refused naming the key and the terminal command; an agent cannot set the key through `config set` (the guard on `permissions.mcp…` covers `permissions.bot.api`). Breaking for a CLI: `generatedApiCommand` no longer marks the group `mcp: false`, and each operation now declares effects (`remote: read` or `write`), so `commands --json` shows them and a catalogue built with `toolsFromCommands` without `levels` lists the operations; update a pinned bot catalogue and a document that lists what MCP leaves out. Under `<PREFIX>_READ_ONLY` a write operation now refuses on the command line too. The command line's `bot api` is otherwise unchanged.
15
+
16
+ ### Fixed
17
+
18
+ - **Voice-note search no longer turns an English word into another real English word.** A word in a transcript widens to the spellings speech recognition garbles only past a dictionary of its language: Russian as before, and now English and Spanish, following the store's word-form languages (`search_stemmers`). Before, «cost» also found «cast». A Latin word with no dictionary for its language, and English and Spanish number words, no longer widen.
19
+
10
20
  ## 0.401.0 — 24.10.2026
11
21
 
12
22
  ### Changed — may break callers
@@ -1,15 +1,18 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command, Option } from "commander";
4
+ import { REMOTE_READ, REMOTE_WRITE } from "../effects.js";
4
5
  import { apiFlagOf, apiOptionKey, readApiBody } from "./api-input.js";
5
6
  export const generatedApiCommand = (api) => {
6
- const group = annotate(new Command("api"), { mcp: false }).description(api.description ?? "every operation of the official Bot API, generated from its schema");
7
+ const group = new Command("api").description(api.description ?? "every operation of the official Bot API, generated from its schema");
7
8
  group.option("--store-token <profile>", "keep a returned authentication token only in this bot profile's OS keyring; never print it");
8
9
  for (const operation of api.operations) {
9
10
  const binding = operation.binding.kind === "http"
10
11
  ? `${operation.binding.method} ${operation.binding.path}`
11
12
  : operation.binding.name;
12
- const command = new Command(operation.command).description(`${operation.summary ?? operation.id} — ${operation.effect} (${binding})`);
13
+ const command = annotate(new Command(operation.command), {
14
+ effects: operation.effect === "read" ? REMOTE_READ : REMOTE_WRITE,
15
+ }).description(`${operation.summary ?? operation.id} — ${operation.effect} (${binding})`);
13
16
  for (const parameter of operation.parameters) {
14
17
  if (parameter.sensitive)
15
18
  continue;
@@ -2,7 +2,7 @@ import { CliError, pathsAreOverridden } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command } from "commander";
4
4
  import { envName } from "../ports/app.js";
5
- import { AUTHORITY_KEY } from "../sends/permissions.js";
5
+ import { AUTHORITY_KEY, BOT_API_KEY } from "../sends/permissions.js";
6
6
  import { loadSnakeCaseConfig } from "./config-keys.js";
7
7
  import { baseContext, environmentOf } from "./context.js";
8
8
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
@@ -96,7 +96,7 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
96
96
  throw new CliError("permission_error", `this process is locked to profile ${settings.profile} (${lock}) — ${setting === "default_profile" ? setting : "--defaults"} changes other profiles`);
97
97
  }
98
98
  if (environmentOf(this).agent)
99
- refuseAuthorityKey(setting, action === "set" ? String(given) : undefined);
99
+ refuseGuardedKey(setting, action === "set" ? String(given) : undefined);
100
100
  if (action === "set")
101
101
  refuseUnknownKey(this, setting, String(given), permissionKey);
102
102
  const saved = config.changeSetting(settings.configPath, {
@@ -137,10 +137,11 @@ export const refuseUnknownKey = (command, setting, value, permissionKey) => {
137
137
  }
138
138
  };
139
139
  /**
140
- * An agent may hold `mcp.authority` and change other settings, but never the key that let it in: on any profile, in
141
- * the defaults, or by replacing `permissions` whole. Only the owner, in a terminal, turns it on or off (NEED-1662).
140
+ * An agent may hold `mcp.authority` or `bot.api` and change other settings, but never the key that let it in: on any
141
+ * profile, in the defaults, or by replacing `permissions` whole. Only the owner, in a terminal, turns it on or off
142
+ * (NEED-1662, NEED-1679).
142
143
  */
143
- const refuseAuthorityKey = (setting, value) => {
144
+ const refuseGuardedKey = (setting, value) => {
144
145
  const keys = setting.startsWith("permissions.")
145
146
  ? [setting.slice("permissions.".length)]
146
147
  : setting === "permissions"
@@ -148,9 +149,14 @@ const refuseAuthorityKey = (setting, value) => {
148
149
  ? ["mcp"]
149
150
  : objectKeys(value)
150
151
  : [];
151
- if (!keys.some((key) => key === "mcp" || key.startsWith("mcp.")))
152
+ const guarded = keys.some((key) => key === BOT_API_KEY || key.startsWith(`${BOT_API_KEY}.`))
153
+ ? BOT_API_KEY
154
+ : keys.some((key) => key === "mcp" || key.startsWith("mcp."))
155
+ ? AUTHORITY_KEY
156
+ : undefined;
157
+ if (!guarded)
152
158
  return;
153
- throw new CliError("permission_error", `an agent cannot change permissions.${AUTHORITY_KEY} — the owner sets it in a terminal`, { permission: AUTHORITY_KEY });
159
+ throw new CliError("permission_error", `an agent cannot change permissions.${guarded} — the owner sets it in a terminal`, { permission: guarded });
154
160
  };
155
161
  /** The keys of a `permissions` object given whole; anything else is left to the schema to refuse. */
156
162
  const objectKeys = (value) => {
@@ -87,6 +87,8 @@ export interface AgentRun {
87
87
  overrides?: readonly string[];
88
88
  /** The server's levels allow `mcp.authority`: a command that changes what a later run may do may run. */
89
89
  authority?: boolean;
90
+ /** The server's levels allow `bot.api`: a raw Bot API call, past the send guard, may run. */
91
+ botApi?: boolean;
90
92
  }
91
93
  export interface RelayedRun {
92
94
  /** The profile serve holds; a command for another is refused before it touches the connection. */
@@ -11,7 +11,7 @@ import { failureContext } from "../errors/reasons.js";
11
11
  import { envName, readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
12
12
  import { recorded, wasSettled } from "../runs/recording.js";
13
13
  import { processRun, useAsProcessRun } from "../runs/scope.js";
14
- import { AUTHORITY_KEY, keyForCommand } from "../sends/permissions.js";
14
+ import { AUTHORITY_KEY, BOT_API_KEY, isBotApi, keyForCommand } from "../sends/permissions.js";
15
15
  import { logParsedArgv, logVersion } from "./argv-log.js";
16
16
  import { commandPathOf } from "./command-contract.js";
17
17
  import { outputFor, provide } from "./context.js";
@@ -258,6 +258,10 @@ const runOnce = async (argv, definition, options) => {
258
258
  throw new CliError("permission_error", `\`${command} ${path.join(" ")}\` changes what a later run may do, and this profile keeps that from agents ` +
259
259
  `(${AUTHORITY_KEY}) — the owner turns it on in a terminal: ` +
260
260
  `\`${command} <profile> config set${path[0] === "bot" ? " --bot" : ""} permissions.${AUTHORITY_KEY} allow\``, { permission: AUTHORITY_KEY, sent: false });
261
+ if (options.agent && options.agent.botApi !== true && isBotApi(path))
262
+ throw new CliError("permission_error", `\`${command} ${path.join(" ")}\` calls the Bot API directly, past the send guard and the recipient list, ` +
263
+ `and this profile keeps that from agents (${BOT_API_KEY}) — the owner turns it on in a terminal: ` +
264
+ `\`${command} <profile> config set --bot permissions.${BOT_API_KEY} allow\``, { permission: BOT_API_KEY, sent: false });
261
265
  const { lifetime, cooperative: stopsAtItsPace } = metaOf(action);
262
266
  const persistent = lifetime === "persistent";
263
267
  listens = persistent;
package/dist/mcp/agent.js CHANGED
@@ -6,7 +6,7 @@ import { VOCABULARY } from "@wirecat/cli-base/standards/tables";
6
6
  import * as v from "valibot";
7
7
  import { changesMessenger } from "../cli/effects.js";
8
8
  import { lentConnection } from "../cli/messenger/serve-relay.js";
9
- import { AUTHORITY_KEY, authorityAllowed, keyForCommand, levelFor, readKeysForCommand, } from "../sends/permissions.js";
9
+ import { AUTHORITY_KEY, authorityAllowed, botApiAllowed, isBotApi, keyForCommand, levelFor, readKeysForCommand, } from "../sends/permissions.js";
10
10
  const TYPES = new Map(VOCABULARY.flags.map((flag) => [flag.name, flag.type]));
11
11
  // `<name|id>` is one argument: its field is `name_or_id`.
12
12
  const snake = (name) => name.replaceAll("|", "_or_").replace(/[^a-z0-9]+/gi, "_");
@@ -150,7 +150,8 @@ export const toolsFromCommands = (commands, options = {}) => {
150
150
  info.mcp !== false &&
151
151
  info.lifetime === undefined &&
152
152
  !(readOnly && (changesMessenger(info) || authority?.(info.path))) &&
153
- (levels === undefined || refusal(info, levels, keyOf, authority) === undefined))
153
+ (levels === undefined ||
154
+ (refusal(info, levels, keyOf, authority) === undefined && (!isBotApi(info.path) || botApiAllowed(levels)))))
154
155
  .map((info) => ({
155
156
  command: info.path.slice(root.length).join(" "),
156
157
  path: info.path,
@@ -230,6 +231,7 @@ export const runCommandForAgent = async (program, path, input, ctx) => {
230
231
  },
231
232
  ...(ctx.overrides?.length ? { overrides: ctx.overrides } : {}),
232
233
  authority: levels !== undefined && authorityAllowed(levels),
234
+ botApi: levels !== undefined && botApiAllowed(levels),
233
235
  },
234
236
  // Opened only when the command asks: a read the store answers never connects.
235
237
  ...(lend
@@ -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));
@@ -21,6 +21,15 @@ 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
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. */
@@ -107,6 +107,18 @@ export const authorityAllowed = (levels) => {
107
107
  const { level } = levelFor(levels, AUTHORITY_KEY);
108
108
  return level === "allow" || level === "ask";
109
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
+ };
110
122
  /** The resources at the top of the command tree, which `readOnly` and `allow` turn read-only as a whole. */
111
123
  export const RESOURCES = [
112
124
  "messages",
@@ -1,5 +1,5 @@
1
+ import { dictionaryLanguages, knownWord } from "../../search/dictionary.js";
1
2
  import { isNumberWord, widenWords } from "../../search/garbled.js";
2
- import { knownRussianWord } from "../../search/russian-dictionary.js";
3
3
  import { createStemmer, DEFAULT_STEMMERS, STEM_SEPARATOR } from "../../search/stem.js";
4
4
  import { normalize } from "../normalize.js";
5
5
  import { ftsMatch, quoted, SPEECH, stemMatch } from "./lucene-fields.js";
@@ -9,6 +9,13 @@ const predicates = (node) => node.kind === "predicate" ? [node] : node.clauses.f
9
9
  const readsTranscripts = (node) => (node.operator === "term" || node.operator === "phrase") &&
10
10
  /[\p{L}\p{N}]/u.test(normalize(node.value)) &&
11
11
  (node.field === "transcript" || (node.implicit === true && (node.field === "text" || node.field === "exact")));
12
+ /** `isNumberWord` knows Russian numbers only; a widened number is a wrong fact in any language. */
13
+ const LATIN_NUMBERS = new Set(`zero one two three four five six seven eight nine ten eleven twelve twenty thirty forty fifty sixty seventy
14
+ eighty ninety hundred thousand million billion first second third fourth fifth sixth seventh eighth ninth tenth
15
+ half quarter cero uno una dos tres cuatro cinco seis siete ocho nueve diez once doce veinte treinta cuarenta
16
+ cincuenta sesenta setenta ochenta noventa cien ciento mil millon primero segundo tercero cuarto quinto medio`.split(/\s+/));
17
+ /** A word widens when its alphabet has a dictionary to tell a garbled spelling from another real word. */
18
+ const widens = (word, covered) => covered(word) && !isNumberWord(word) && !LATIN_NUMBERS.has(word);
12
19
  /**
13
20
  * Each leaf that reads transcripts, with what it matches there. A transcript always widens, not only when nothing
14
21
  * matched as typed text does: a correct and a garbled spelling sit in one chat. Only a single word widens —
@@ -20,7 +27,9 @@ export const transcriptTerms = (database, root, item) => {
20
27
  const saved = savedStemmers(database);
21
28
  if (item !== "message" || !leaves.length || saved === null)
22
29
  return terms;
23
- const stemmer = createStemmer(saved ?? DEFAULT_STEMMERS);
30
+ const stemmers = saved ?? DEFAULT_STEMMERS;
31
+ const stemmer = createStemmer(stemmers);
32
+ const dictionary = knownWord(dictionaryLanguages(stemmers));
24
33
  const stemsOf = (word) => [...new Set(stemmer.explain(word).map((part) => part.stem))];
25
34
  const widening = [];
26
35
  for (const [node, one] of terms) {
@@ -31,8 +40,9 @@ export const transcriptTerms = (database, root, item) => {
31
40
  if (stems)
32
41
  Object.assign(one, { stems: node.slop === undefined ? stems : `(${stems}) NOT ${quoted(STEM_SEPARATOR)}` });
33
42
  const words = normalize(node.value).match(/[\p{L}\p{N}]+/gu) ?? [];
34
- if (node.operator === "term" && words.length === 1 && !isNumberWord(words[0]))
35
- widening.push({ node, literal: words[0], stems: stemsOf(node.value) });
43
+ const [word] = words;
44
+ if (node.operator === "term" && word !== undefined && words.length === 1 && widens(word, dictionary.covers))
45
+ widening.push({ node, literal: word, stems: stemsOf(node.value) });
36
46
  }
37
47
  if (!widening.length)
38
48
  return terms;
@@ -40,7 +50,7 @@ export const transcriptTerms = (database, root, item) => {
40
50
  .prepare("SELECT term, doc FROM transcript_words_vocab WHERE col = 'normalized_text'")
41
51
  .all()
42
52
  .map((row) => ({ term: String(row.term), docs: Number(row.doc) }));
43
- widenWords(widening, vocabulary, stemsOf, knownRussianWord).forEach((words, at) => {
53
+ widenWords(widening, vocabulary, stemsOf, dictionary.known).forEach((words, at) => {
44
54
  const leaf = widening[at];
45
55
  if (leaf)
46
56
  Object.assign(terms.get(leaf.node), { widened: words });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.401.0",
3
+ "version": "0.402.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",
@@ -173,6 +173,8 @@
173
173
  "@xmldom/xmldom": "0.9.12",
174
174
  "chardet": "2.2.0",
175
175
  "defuddle": "0.19.5",
176
+ "dictionary-en": "4.0.0",
177
+ "dictionary-es": "4.0.0",
176
178
  "dictionary-ru": "3.0.0",
177
179
  "echarts": "6.1.0",
178
180
  "fflate": "0.8.3",