@wirecat/cli-core 0.421.0 → 0.422.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 +11 -0
- package/dist/cli/commands-command.js +1 -1
- package/dist/cli/config-command.d.ts +0 -4
- package/dist/cli/config-command.js +4 -2
- package/dist/cli/first-run.d.ts +0 -16
- package/dist/cli/first-run.js +1 -6
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/profile.d.ts +2 -0
- package/dist/cli/profile.js +10 -0
- package/dist/cli/program.js +11 -7
- package/dist/mcp/surface.js +9 -3
- package/dist/ports/app.d.ts +6 -0
- package/dist/search/dictionary.d.ts +3 -6
- package/dist/search/dictionary.js +2 -1
- package/dist/search/languages.d.ts +28 -0
- package/dist/search/languages.js +7 -0
- package/dist/search/stem.d.ts +5 -4
- package/dist/search/stem.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.422.0 — 25.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`AppIdentity.messenger`: set it to `false` in a CLI that is not a messenger** (a notes, files or calendar CLI from cli-template). The shared program, `commands`, `config` and MCP text then stop speaking of chats and messages: the profile example in the program description names the CLI's own first command, `commands info`'s example is `<cmd> commands info commands`, `--brief` and `--full` describe "found items", `--trace` and `--record` say "never content", and `config show` and the `config set`/`unset` list leave out `sender_colors`. Unset means a messenger: tg, max and wa print exactly what they did. A CLI that sets it refreshes its contract snapshots once.
|
|
15
|
+
- **The MCP `_tools_search`, `_read` and `_write` tools speak neutrally when the catalogue has no `messages` commands**: task words "list", "show", "change a setting", one of the CLI's own reading commands as the example, and "creates, edits, deletes" for writes. No setting is needed; a messenger's catalogue is unchanged.
|
|
16
|
+
|
|
17
|
+
### Changed — may break callers
|
|
18
|
+
|
|
19
|
+
- **The languages search stems are one table, `LANGUAGES`, and each entry now names its Hunspell dictionary** (`dictionary: "dictionary-ru"`). `LANGUAGES` from `/cli` keeps its other fields. `LATIN_STEMMERS` is now `english, spanish, none`, in the order the error for an unknown stemmer already printed, so the `allowed` list of a `stemmer.invalid` error comes in that order too. A caller that compared that list in order sorts it first.
|
|
20
|
+
|
|
10
21
|
## 0.421.0 — 25.10.2026
|
|
11
22
|
|
|
12
23
|
### Changed — may break callers
|
|
@@ -59,7 +59,7 @@ export const commandsCommand = (app) => {
|
|
|
59
59
|
});
|
|
60
60
|
command.addCommand(annotate(new Command("info"), {
|
|
61
61
|
effects: NO_EFFECTS,
|
|
62
|
-
examples: [`${app.command} commands info messages list`],
|
|
62
|
+
examples: [`${app.command} commands info ${app.messenger === false ? "commands" : "messages list"}`],
|
|
63
63
|
})
|
|
64
64
|
.description(DISCOVERY_DESCRIPTIONS.info)
|
|
65
65
|
.argument("[path...]", "one command path; without one, export schemas, error reasons and explicit overrides")
|
|
@@ -2,10 +2,6 @@ import { Command } from "commander";
|
|
|
2
2
|
import { type AppIdentity } from "../ports/app.js";
|
|
3
3
|
import { type PermissionKeyOf } from "./permission-keys.js";
|
|
4
4
|
import { type Configuration } from "./settings.js";
|
|
5
|
-
/**
|
|
6
|
-
* The settings in force and where each came from, and changing them. Printed whole: no field the
|
|
7
|
-
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
8
|
-
*/
|
|
9
5
|
export declare const configCommand: (app: AppIdentity, config: Configuration, { permissionKey }?: {
|
|
10
6
|
permissionKey?: PermissionKeyOf;
|
|
11
7
|
}) => Command;
|
|
@@ -15,6 +15,8 @@ import { changeStoreSetting, isStoreSetting, STORE_SETTINGS, storeSettings } fro
|
|
|
15
15
|
* The settings in force and where each came from, and changing them. Printed whole: no field the
|
|
16
16
|
* configuration accepts can hold a secret — the schema has nowhere to put one.
|
|
17
17
|
*/
|
|
18
|
+
/** `sender_colors` colours who wrote a message; a CLI without messages has nothing to colour. */
|
|
19
|
+
const shownSettings = (app, config) => app.messenger === false ? config.allSettings.filter((setting) => setting !== "sender_colors") : config.allSettings;
|
|
18
20
|
export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
19
21
|
const command = new Command("config").description("the settings in force, and where each one came from");
|
|
20
22
|
annotate(command.command("show"), { effects: LOCAL_READ, examples: [`${app.command} personal config show`] })
|
|
@@ -32,7 +34,7 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
|
32
34
|
config_file: settings.configPath,
|
|
33
35
|
config_found: settings.configFound,
|
|
34
36
|
paths_overridden: overridden,
|
|
35
|
-
settings: [...config
|
|
37
|
+
settings: [...shownSettings(app, config), "command_timeout_ms"]
|
|
36
38
|
.filter((setting) => bot || setting !== "read_other_bots")
|
|
37
39
|
.map((setting) => sourced(settings, setting, config.schema)),
|
|
38
40
|
store_settings: await storeSettings(env),
|
|
@@ -49,7 +51,7 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
|
|
|
49
51
|
}))
|
|
50
52
|
.argument("<setting>",
|
|
51
53
|
// default_profile is only removed now: `config unset` still takes it, `config set` refuses it.
|
|
52
|
-
`one of: ${[...config.
|
|
54
|
+
`one of: ${[...shownSettings(app, config).filter((one) => action === "unset" || one !== "default_profile"), ...STORE_SETTINGS, "account_groups.<name>"].join(", ")}`)
|
|
53
55
|
.option("--defaults", "change what every profile gets, rather than this profile")
|
|
54
56
|
.option("--personal", "only for personal accounts — the personal section of the file")
|
|
55
57
|
.option("--bot", "only for bots — the bot section of the file");
|
package/dist/cli/first-run.d.ts
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { type Installer } from "@wirecat/cli-base/update";
|
|
2
2
|
import { type Question } from "./setup-answers.js";
|
|
3
3
|
/** The languages cli-core stems and has a dictionary for; each is one stemmer of its alphabet. */
|
|
4
|
-
export declare const LANGUAGES: readonly [{
|
|
5
|
-
readonly name: "russian";
|
|
6
|
-
readonly code: "ru";
|
|
7
|
-
readonly label: "Russian";
|
|
8
|
-
readonly script: "cyrillic";
|
|
9
|
-
}, {
|
|
10
|
-
readonly name: "english";
|
|
11
|
-
readonly code: "en";
|
|
12
|
-
readonly label: "English";
|
|
13
|
-
readonly script: "latin";
|
|
14
|
-
}, {
|
|
15
|
-
readonly name: "spanish";
|
|
16
|
-
readonly code: "es";
|
|
17
|
-
readonly label: "Spanish";
|
|
18
|
-
readonly script: "latin";
|
|
19
|
-
}];
|
|
20
4
|
export interface FirstRunIo {
|
|
21
5
|
ask: (prompt: string) => Promise<string>;
|
|
22
6
|
say: (text: string) => void;
|
package/dist/cli/first-run.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { accessSync, constants, realpathSync } from "node:fs";
|
|
3
3
|
import { delimiter, join } from "node:path";
|
|
4
4
|
import { installerOf, updateCommand } from "@wirecat/cli-base/update";
|
|
5
|
+
import { LANGUAGES } from "../search/languages.js";
|
|
5
6
|
import { DEFAULT_STEMMERS } from "../search/stem.js";
|
|
6
7
|
import { openCache } from "../store/open.js";
|
|
7
8
|
import { storePath } from "../store/path.js";
|
|
@@ -11,12 +12,6 @@ import { readSecret } from "../terminal/prompt.js";
|
|
|
11
12
|
import { WIRECAT_TOOLS } from "./help/first-screen.js";
|
|
12
13
|
import { buildStems } from "./messenger/store-maintenance-command.js";
|
|
13
14
|
import { QUESTIONS, readSetupAnswers, unanswered, writeSetupAnswers, } from "./setup-answers.js";
|
|
14
|
-
/** The languages cli-core stems and has a dictionary for; each is one stemmer of its alphabet. */
|
|
15
|
-
export const LANGUAGES = [
|
|
16
|
-
{ name: "russian", code: "ru", label: "Russian", script: "cyrillic" },
|
|
17
|
-
{ name: "english", code: "en", label: "English", script: "latin" },
|
|
18
|
-
{ name: "spanish", code: "es", label: "Spanish", script: "latin" },
|
|
19
|
-
];
|
|
20
15
|
/** Commands that run without first-run questions: they serve agents, help, or open the store themselves. */
|
|
21
16
|
const SKIPPED = new Set(["help", "guide", "completion", "mcp", "serve", "server", "update", "store"]);
|
|
22
17
|
/**
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export { CORE_LOG_EVENTS } from "../runs/log-events.js";
|
|
|
26
26
|
export { crashOf, type Recording, type RecordingOptions, recorded, startRecording, wasSettled, } from "../runs/recording.js";
|
|
27
27
|
export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, type RunMetadata, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
|
|
28
28
|
export { currentRun, inRun, type RunScope, useAsProcessRun } from "../runs/scope.js";
|
|
29
|
+
export { LANGUAGES } from "../search/languages.js";
|
|
29
30
|
export { guardedApp, guardedPress, guardedStart, type Pressed } from "../sends/buttons.js";
|
|
30
31
|
export { guardedClose, guardedCreatePoll, guardedVote } from "../sends/polls.js";
|
|
31
32
|
export type { InboxReader } from "../services/inbox.js";
|
|
@@ -62,7 +63,7 @@ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNoth
|
|
|
62
63
|
export { type CliEntry, type EntryContext, type EntryIo, startCli } from "./entry.js";
|
|
63
64
|
export { type AnswerOutput, answerSchema, type Body, type ItemBody, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
64
65
|
export { withAbort } from "./execution.js";
|
|
65
|
-
export { askSetupQuestions, asksAtStart, type FirstRun, type FirstRunIo,
|
|
66
|
+
export { askSetupQuestions, asksAtStart, type FirstRun, type FirstRunIo, setupAtStart, systemLanguage, } from "./first-run.js";
|
|
66
67
|
export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, loadScenarios, MEMO_SCENARIOS, type Scenario, shellProblems, } from "./guide.js";
|
|
67
68
|
export { type CommandHelp, commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, type NextChoice, nextUsually, } from "./help/command-help.js";
|
|
68
69
|
export { commandsText } from "./help/commands-text.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -22,6 +22,7 @@ export { CORE_LOG_EVENTS } from "../runs/log-events.js";
|
|
|
22
22
|
export { crashOf, recorded, startRecording, wasSettled, } from "../runs/recording.js";
|
|
23
23
|
export { findRun, KEEP_RUNS_FOR_DAYS, listRuns, pruneRuns, readEvents, runsDirFor, runtime, startRun, } from "../runs/run.js";
|
|
24
24
|
export { currentRun, inRun, useAsProcessRun } from "../runs/scope.js";
|
|
25
|
+
export { LANGUAGES } from "../search/languages.js";
|
|
25
26
|
export { guardedApp, guardedPress, guardedStart } from "../sends/buttons.js";
|
|
26
27
|
export { guardedClose, guardedCreatePoll, guardedVote } from "../sends/polls.js";
|
|
27
28
|
export { INBOX_CHATS, newIn, REVIEW_CHATS, reviewIn, unreadIn } from "../services/inbox.js";
|
|
@@ -55,7 +56,7 @@ export { changesAuthority, changesAuthorityAt, changesAuthorityNow, destroysNoth
|
|
|
55
56
|
export { startCli } from "./entry.js";
|
|
56
57
|
export { answerSchema, isBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
|
|
57
58
|
export { withAbort } from "./execution.js";
|
|
58
|
-
export { askSetupQuestions, asksAtStart,
|
|
59
|
+
export { askSetupQuestions, asksAtStart, setupAtStart, systemLanguage, } from "./first-run.js";
|
|
59
60
|
export { checkGuide, checkStep, loadScenarios, MEMO_SCENARIOS, shellProblems, } from "./guide.js";
|
|
60
61
|
export { commandHelp, commandHelpMarkdown, commandHelpText, exampleReport, nextUsually, } from "./help/command-help.js";
|
|
61
62
|
export { commandsText } from "./help/commands-text.js";
|
package/dist/cli/profile.d.ts
CHANGED
|
@@ -28,3 +28,5 @@ export declare const valuedOptions: (program: Command) => Set<string>;
|
|
|
28
28
|
export declare const refuseCommandName: (profile: string, words: Set<string>, command: string) => void;
|
|
29
29
|
/** The root of the command tree, from anywhere inside it. */
|
|
30
30
|
export declare const rootOf: (command: Command) => Command;
|
|
31
|
+
/** A command this CLI really has, for an example: the first that takes a profile, down to its first leaf. */
|
|
32
|
+
export declare const sampleCommand: (commands: readonly Command[]) => string | undefined;
|
package/dist/cli/profile.js
CHANGED
|
@@ -59,3 +59,13 @@ export const rootOf = (command) => {
|
|
|
59
59
|
root = root.parent;
|
|
60
60
|
return root;
|
|
61
61
|
};
|
|
62
|
+
/** A command this CLI really has, for an example: the first that takes a profile, down to its first leaf. */
|
|
63
|
+
export const sampleCommand = (commands) => {
|
|
64
|
+
const words = [];
|
|
65
|
+
let command = commands.find((one) => !PROFILE_FREE.has(one.name()));
|
|
66
|
+
while (command) {
|
|
67
|
+
words.push(command.name());
|
|
68
|
+
command = command.commands.find((one) => one.name() !== "help");
|
|
69
|
+
}
|
|
70
|
+
return words.length ? words.join(" ") : undefined;
|
|
71
|
+
};
|
package/dist/cli/program.js
CHANGED
|
@@ -26,7 +26,7 @@ import { showInputFields } from "./input-fields.js";
|
|
|
26
26
|
import { MAX_BUFFERED_INPUT, provideInputPolicy } from "./input-policy.js";
|
|
27
27
|
import { checkJson, liftJson } from "./json-fields.js";
|
|
28
28
|
import { PreviewComplete, preview } from "./preview.js";
|
|
29
|
-
import { asFirstWord, commandWords, liftProfile, PROFILE_FREE, valuedOptions } from "./profile.js";
|
|
29
|
+
import { asFirstWord, commandWords, liftProfile, PROFILE_FREE, sampleCommand, valuedOptions } from "./profile.js";
|
|
30
30
|
import { absolutePaths, relayTarget } from "./relay-route.js";
|
|
31
31
|
import { settingsFor } from "./settings.js";
|
|
32
32
|
import { assertSharedContracts } from "./shared-contracts.js";
|
|
@@ -37,11 +37,15 @@ import { commandAt, lineOf, withLines } from "./steps.js";
|
|
|
37
37
|
*/
|
|
38
38
|
export const createProgram = ({ app, commands, configure, firstScreen }, { out, err } = {}) => {
|
|
39
39
|
const program = new Command();
|
|
40
|
+
const mounted = commands();
|
|
41
|
+
const messenger = app.messenger !== false;
|
|
42
|
+
const found = messenger ? "found messages" : "found items";
|
|
43
|
+
const content = messenger ? "message content" : "content";
|
|
40
44
|
program
|
|
41
45
|
.name(app.command)
|
|
42
46
|
.usage("[profile] [options] <command>")
|
|
43
47
|
.description(`${app.description}\n\n` +
|
|
44
|
-
`The first word is the profile whenever it is not a command — \`${app.command} personal chats list\`.\n` +
|
|
48
|
+
`The first word is the profile whenever it is not a command — \`${app.command} personal ${messenger ? "chats list" : (sampleCommand(mounted) ?? "<command>")}\`.\n` +
|
|
45
49
|
`\`${app.envPrefix}_PROFILE\` says the same thing for a whole shell session.\n` +
|
|
46
50
|
"No profile is assumed: without one, a command that uses an account stops and lists the profiles here.")
|
|
47
51
|
.version(app.version, "-V, --version")
|
|
@@ -49,21 +53,21 @@ export const createProgram = ({ app, commands, configure, firstScreen }, { out,
|
|
|
49
53
|
.option("--json", "machine-readable output: one JSON value on stdout; --json=id,text keeps only those fields")
|
|
50
54
|
.option("--jsonl", "machine-readable output: one JSON object per line, for streaming and jq")
|
|
51
55
|
.option("--quiet", "diagnostics off; a failure is still said")
|
|
52
|
-
.option("--trace",
|
|
56
|
+
.option("--trace", `the connection's own log lines on stderr — never ${content}`)
|
|
53
57
|
.option("--debug", "on a failure, its whole record — cause and stack — in place of the short line")
|
|
54
58
|
.option("--timeout <duration>", "give up on the whole command after this — 30s, 2m, 500ms")
|
|
55
59
|
.option("--offline", "answer from what was recorded and never connect; fails if nothing was")
|
|
56
60
|
.option("--no-input", "never prompt or open interactive login; piped input remains available")
|
|
57
61
|
.option("--max-input-bytes <bytes>", "maximum buffered input bytes (default: 16777216)")
|
|
58
62
|
.option("--max-output-bytes <bytes>", "maximum machine output bytes (default: 4194304; 0 disables)")
|
|
59
|
-
.option("--brief",
|
|
60
|
-
.option("--full",
|
|
63
|
+
.option("--brief", `${found} as reference, title, time, ${messenger ? "sender" : "author"} and excerpt only`)
|
|
64
|
+
.option("--full", `${found} with their whole text and every provider field`)
|
|
61
65
|
.option("--dry-run", "preview parsed arguments and permissions before running the action")
|
|
62
66
|
.option("--yes", "go ahead without the question an ask level puts before a write")
|
|
63
|
-
.option("--record",
|
|
67
|
+
.option("--record", `keep this run — ids and timings, never ${content}`)
|
|
64
68
|
.option("--no-record", "do not keep it, whatever the configuration says")
|
|
65
69
|
.showHelpAfterError();
|
|
66
|
-
for (const command of
|
|
70
|
+
for (const command of mounted)
|
|
67
71
|
program.addCommand(command);
|
|
68
72
|
configure?.(program);
|
|
69
73
|
assertSharedContracts(program);
|
package/dist/mcp/surface.js
CHANGED
|
@@ -62,10 +62,16 @@ export const registerCommands = (server, prefix, commands, errors,
|
|
|
62
62
|
/** Throws the refusal of a command left out because the lock or a level stops it, so the agent learns why. */
|
|
63
63
|
withheld) => {
|
|
64
64
|
const byNormal = new Map(Object.keys(commands).map((command) => [normal(command), command]));
|
|
65
|
+
// No AppIdentity reaches here; only a messenger's catalogue has `messages` commands.
|
|
66
|
+
const messenger = Object.keys(commands).some((command) => command.split(" ")[0] === "messages");
|
|
67
|
+
const tasks = messenger ? '"unread", "send message", "chat members"' : '"list", "show", "change a setting"';
|
|
68
|
+
const sample = messenger
|
|
69
|
+
? "messages list"
|
|
70
|
+
: (Object.entries(commands).find(([, one]) => !one.writes)?.[0] ?? Object.keys(commands)[0] ?? "");
|
|
65
71
|
server.registerTool(`${prefix}_tools_search`, {
|
|
66
72
|
outputSchema: toStandardJsonSchema(v.looseObject({})),
|
|
67
73
|
title: "Find a command",
|
|
68
|
-
description: `Find the command for a task —
|
|
74
|
+
description: `Find the command for a task — ${tasks} — before calling ` +
|
|
69
75
|
`${prefix}_read or ${prefix}_write with it. Each match gives its command, whether it writes, its arguments and the shape of its answer. ` +
|
|
70
76
|
"With no words, lists every command and title, without arguments.",
|
|
71
77
|
inputSchema: toStandardJsonSchema(v.strictObject({ query: v.optional(v.pipe(v.string(), v.description("words that describe the task"))) })),
|
|
@@ -122,7 +128,7 @@ withheld) => {
|
|
|
122
128
|
}
|
|
123
129
|
};
|
|
124
130
|
const call = v.strictObject({
|
|
125
|
-
command: v.pipe(v.string(), v.minLength(1), v.description(`a command ${prefix}_tools_search returned: "
|
|
131
|
+
command: v.pipe(v.string(), v.minLength(1), v.description(`a command ${prefix}_tools_search returned: "${sample}"`)),
|
|
126
132
|
arguments: v.optional(v.pipe(v.record(v.string(), v.unknown()), v.description("that command's arguments"))),
|
|
127
133
|
});
|
|
128
134
|
server.registerTool(`${prefix}_read`, {
|
|
@@ -136,7 +142,7 @@ withheld) => {
|
|
|
136
142
|
server.registerTool(`${prefix}_write`, {
|
|
137
143
|
outputSchema: toStandardJsonSchema(ANSWER),
|
|
138
144
|
title: "Run a writing command",
|
|
139
|
-
description: `Run one command that changes something — sends, edits, deletes, joins — with the arguments ${prefix}_tools_search showed. ${UNTRUSTED}`,
|
|
145
|
+
description: `Run one command that changes something — ${messenger ? "sends, edits, deletes, joins" : "creates, edits, deletes"} — with the arguments ${prefix}_tools_search showed. ${UNTRUSTED}`,
|
|
140
146
|
inputSchema: toStandardJsonSchema(call),
|
|
141
147
|
annotations: WRITE,
|
|
142
148
|
}, runner(true));
|
package/dist/ports/app.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface AppIdentity {
|
|
|
19
19
|
* can stem in place of English and the system's language; the person still confirms, since the choice is shared.
|
|
20
20
|
*/
|
|
21
21
|
contentLanguages?: readonly string[];
|
|
22
|
+
/**
|
|
23
|
+
* `false` for a CLI that is not a messenger — notes, files, a calendar. The shared help, `config`, `commands info`
|
|
24
|
+
* and MCP text then name its own commands and leave out chats, messages and senders. Unset means a messenger, so
|
|
25
|
+
* tg, max and wa print what they always did.
|
|
26
|
+
*/
|
|
27
|
+
messenger?: boolean;
|
|
22
28
|
/** Error reasons and detail keys of its own — max's `max_code`, tg's `migrated_chat_id` — kept in every error it prints. */
|
|
23
29
|
errors?: AppErrors;
|
|
24
30
|
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
+
import { type Language } from "./languages.js";
|
|
1
2
|
/** The Hunspell dictionary that tells a real word from a garbled one, for each language the store stems. */
|
|
2
|
-
export declare const DICTIONARIES:
|
|
3
|
-
|
|
4
|
-
readonly english: "dictionary-en";
|
|
5
|
-
readonly spanish: "dictionary-es";
|
|
6
|
-
};
|
|
7
|
-
export type DictionaryLanguage = keyof typeof DICTIONARIES;
|
|
3
|
+
export declare const DICTIONARIES: Record<Language, string>;
|
|
4
|
+
export type DictionaryLanguage = Language;
|
|
8
5
|
/**
|
|
9
6
|
* `known`: whether a folded word the index stores is real in one of `languages` that write in its alphabet.
|
|
10
7
|
* `covers`: whether any of them does; without one, a garbled spelling cannot be told from a real word.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { fold } from "../store/normalize.js";
|
|
3
|
+
import { LANGUAGES } from "./languages.js";
|
|
3
4
|
/** The Hunspell dictionary that tells a real word from a garbled one, for each language the store stems. */
|
|
4
|
-
export const DICTIONARIES = {
|
|
5
|
+
export const DICTIONARIES = Object.fromEntries(LANGUAGES.map(({ name, dictionary }) => [name, dictionary]));
|
|
5
6
|
/** `fold`, without Unicode normalisation for plain Russian letters: it would double the time Russian loads in. */
|
|
6
7
|
const folded = (word) => /^[а-яё'-]*$/.test(word) ? word.replaceAll("ё", "е").replaceAll("й", "и") : fold(word);
|
|
7
8
|
const loaded = new Map();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The languages search can stem and spell-check. Adding one here needs its Snowball stemmer in `stem.ts`. */
|
|
2
|
+
export declare const LANGUAGES: readonly [{
|
|
3
|
+
readonly name: "russian";
|
|
4
|
+
readonly code: "ru";
|
|
5
|
+
readonly label: "Russian";
|
|
6
|
+
readonly script: "cyrillic";
|
|
7
|
+
readonly dictionary: "dictionary-ru";
|
|
8
|
+
}, {
|
|
9
|
+
readonly name: "english";
|
|
10
|
+
readonly code: "en";
|
|
11
|
+
readonly label: "English";
|
|
12
|
+
readonly script: "latin";
|
|
13
|
+
readonly dictionary: "dictionary-en";
|
|
14
|
+
}, {
|
|
15
|
+
readonly name: "spanish";
|
|
16
|
+
readonly code: "es";
|
|
17
|
+
readonly label: "Spanish";
|
|
18
|
+
readonly script: "latin";
|
|
19
|
+
readonly dictionary: "dictionary-es";
|
|
20
|
+
}];
|
|
21
|
+
type Entry = (typeof LANGUAGES)[number];
|
|
22
|
+
export type Language = Entry["name"];
|
|
23
|
+
export type Script = Entry["script"];
|
|
24
|
+
export type LanguageOf<S extends Script> = Extract<Entry, {
|
|
25
|
+
script: S;
|
|
26
|
+
}>["name"];
|
|
27
|
+
export declare const languagesOf: <S extends Script>(script: S) => LanguageOf<S>[];
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The languages search can stem and spell-check. Adding one here needs its Snowball stemmer in `stem.ts`. */
|
|
2
|
+
export const LANGUAGES = [
|
|
3
|
+
{ name: "russian", code: "ru", label: "Russian", script: "cyrillic", dictionary: "dictionary-ru" },
|
|
4
|
+
{ name: "english", code: "en", label: "English", script: "latin", dictionary: "dictionary-en" },
|
|
5
|
+
{ name: "spanish", code: "es", label: "Spanish", script: "latin", dictionary: "dictionary-es" },
|
|
6
|
+
];
|
|
7
|
+
export const languagesOf = (script) => LANGUAGES.filter((one) => one.script === script).map(({ name }) => name);
|
package/dist/search/stem.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { type LanguageOf } from "./languages.js";
|
|
1
2
|
/** The version of the vendored files in `./snowball/`; `bin/snowball-update` regenerates them. */
|
|
2
3
|
export declare const SNOWBALL_VERSION = "3.1.1";
|
|
3
|
-
export
|
|
4
|
-
export
|
|
5
|
-
export
|
|
6
|
-
export
|
|
4
|
+
export type CyrillicStemmer = LanguageOf<"cyrillic"> | "none";
|
|
5
|
+
export type LatinStemmer = LanguageOf<"latin"> | "none";
|
|
6
|
+
export declare const CYRILLIC_STEMMERS: readonly CyrillicStemmer[];
|
|
7
|
+
export declare const LATIN_STEMMERS: readonly LatinStemmer[];
|
|
7
8
|
/** `latin` is one stemmer, `none`, or several joined by commas in alphabetical order (`english,spanish`). */
|
|
8
9
|
export type Stemmers = {
|
|
9
10
|
cyrillic: CyrillicStemmer;
|
package/dist/search/stem.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { normalize } from "../store/normalize.js";
|
|
3
|
+
import { languagesOf } from "./languages.js";
|
|
3
4
|
import EnglishStemmer from "./snowball/english-stemmer.js";
|
|
4
5
|
import RussianStemmer from "./snowball/russian-stemmer.js";
|
|
5
6
|
import SpanishStemmer from "./snowball/spanish-stemmer.js";
|
|
6
7
|
/** The version of the vendored files in `./snowball/`; `bin/snowball-update` regenerates them. */
|
|
7
8
|
export const SNOWBALL_VERSION = "3.1.1";
|
|
8
|
-
export const CYRILLIC_STEMMERS = ["
|
|
9
|
-
export const LATIN_STEMMERS = ["
|
|
9
|
+
export const CYRILLIC_STEMMERS = [...languagesOf("cyrillic"), "none"];
|
|
10
|
+
export const LATIN_STEMMERS = [...languagesOf("latin"), "none"];
|
|
10
11
|
export const DEFAULT_STEMMERS = { cyrillic: "russian", latin: "english,spanish" };
|
|
11
12
|
/**
|
|
12
13
|
* Raised with every change to `DEFAULT_STEMMERS`. A default saved in the store carries it, so a newer build
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.422.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",
|