@wirecat/cli-core 0.447.0 → 0.449.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
@@ -5,6 +5,27 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## 0.449.0 — 26.10.2026
9
+
10
+ ### Changed — may break callers
11
+
12
+ - **cli-core needs `@wirecat/cli-base` 0.62.** The peer range moves from `>=0.59.0 <0.60.0` to `>=0.62.0 <0.63.0`. A
13
+ CLI that pins cli-base moves its pin with this release. cli-base 0.62 checks shared commands about twice as fast,
14
+ which shortens every command's setup: in tg a warm `run(["--version"])` went from 49 to 29 ms. cli-base 0.61 says in
15
+ its handbook that a run record keeps the command line as typed, with secrets still hidden; a CLI whose tests or
16
+ documents say otherwise follows its changelog.
17
+
18
+ ## 0.448.0 — 26.10.2026
19
+
20
+ ### Added
21
+
22
+ - **Text models and rerankers run on native ONNX Runtime on Linux (glibc, x64 and arm64) and Apple-silicon Macs**,
23
+ 2–3× faster than WebAssembly: on one real archive `search messages --discover` went from 4.6 s to 3.2 s with the
24
+ same answers. `models download` of a text model or reranker now also fetches Microsoft's ONNX Runtime 1.30.0
25
+ library for the system (10–42 MB, checked by sha256) into `models/runtime/`; an install that already has its
26
+ models gets it on its next `models download`. Windows, Intel Macs, musl, picture models, and any library that
27
+ does not load keep running through WebAssembly. Needs `@wirecat/cli-core-onnx` 1.1.0.
28
+
8
29
  ## 0.447.0 — 26.10.2026
9
30
 
10
31
  ### Added
package/README.md CHANGED
@@ -130,8 +130,9 @@ code and its document must agree. A search plan that reads an index once per mes
130
130
  pattern can hang the search.
131
131
  - **Presets for what people actually look for.** `preset:password`, `api-key`, `card`, `phone`, `email`, `url` and
132
132
  seven more find candidates, labelled as candidates.
133
- - **Search by meaning, opt in.** Conversations are split into chunks and embedded locally by a WebAssembly ONNX
134
- runtime, the same on Node and Bun — e5-small by default, or any OpenAI-compatible API with your own key, after
133
+ - **Search by meaning, opt in.** Conversations are split into chunks and embedded locally by ONNX Runtime — native
134
+ on Linux (glibc) and Apple-silicon Macs once `models download` has fetched it, WebAssembly elsewhere, on Node and
135
+ Bun alike — e5-small by default, or any OpenAI-compatible API with your own key, after
135
136
  you see the cost. Results by meaning and by words are merged by reciprocal rank fusion.
136
137
  - **The archive and the server together.** `--backend both` also asks the messenger's own search; its hits are saved
137
138
  and filtered by the same query, so nothing is counted twice. `--thread` brings the reply thread around a hit.
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { availableParallelism } from "node:os";
3
3
  import { basename, join } from "node:path";
4
4
  import { filesPresent, sharedModelsDirectory } from "../speech/install.js";
5
+ import { onnxRuntime } from "./onnx-runtime.js";
5
6
  export const textModelsDirectory = (env = process.env) => sharedModelsDirectory("text", env);
6
7
  export const placedText = (model, directory) => model.files.map((file) => [file, join(directory, model.id, file.name)]);
7
8
  export const isTextModelInstalled = (model, directory) => filesPresent(placedText(model, directory));
@@ -122,13 +123,29 @@ export const meanOf = (data, tokens, dims) => {
122
123
  };
123
124
  /** A model's tokenizer and ONNX session, from its folder. */
124
125
  export const loadModel = async (model, directory, threads) => {
125
- const [{ Tokenizer }, ort] = await Promise.all([import("@huggingface/tokenizers"), import("@wirecat/cli-core-onnx")]);
126
+ const [{ Tokenizer }, runtime] = await Promise.all([import("@huggingface/tokenizers"), onnxRuntime()]);
126
127
  const path = (name) => join(directory, model.id, name);
127
128
  const tokenizer = new Tokenizer(JSON.parse(readFileSync(path("tokenizer.json"), "utf8")), JSON.parse(readFileSync(path("tokenizer_config.json"), "utf8")));
128
- ort.env.wasm.numThreads = threads;
129
- const session = await ort.InferenceSession.create(readFileSync(path(model.onnx)), model.externalData
129
+ const bytes = readFileSync(path(model.onnx));
130
+ const external = model.externalData
130
131
  ? { externalData: [{ path: basename(model.externalData), data: readFileSync(path(model.externalData)) }] }
131
- : {});
132
+ : {};
133
+ if (runtime.native) {
134
+ try {
135
+ const session = await runtime.ort.InferenceSession.create(bytes, {
136
+ ...external,
137
+ intraOpNumThreads: threads,
138
+ interOpNumThreads: 1,
139
+ });
140
+ return { ort: runtime.ort, tokenizer, session };
141
+ }
142
+ catch {
143
+ // A model the native runtime will not open still runs through WebAssembly.
144
+ }
145
+ }
146
+ const ort = await import("@wirecat/cli-core-onnx");
147
+ ort.env.wasm.numThreads = threads;
148
+ const session = await ort.InferenceSession.create(bytes, external);
132
149
  return { ort, tokenizer, session };
133
150
  };
134
151
  export const openEmbedder = async (model, directory, { threads = defaultThreads() } = {}) => {
@@ -94,7 +94,7 @@ export const RERANK_MODELS = [
94
94
  languages: "about 100 languages, Russian and English among them",
95
95
  licence: "Apache-2.0",
96
96
  maxTokens: 512,
97
- // The x86 name is the quantizer's target; Core runs every model as WebAssembly, on any processor.
97
+ // The x86 name is the quantizer's target; WebAssembly and native ONNX Runtime both run it on any processor.
98
98
  onnx: "onnx/model_quint8_avx2.onnx",
99
99
  files: [
100
100
  {
@@ -0,0 +1,42 @@
1
+ import { type Fetch } from "../speech/install.js";
2
+ import type { ModelFile } from "../speech/models.js";
3
+ export declare const ONNX_RUNTIME_VERSION = "1.30.0";
4
+ /** The library of Microsoft's release, in its archive, and the name the binding looks for beside itself. */
5
+ export interface NativeRuntime {
6
+ readonly target: string;
7
+ readonly archive: ModelFile;
8
+ readonly member: string;
9
+ readonly library: {
10
+ name: string;
11
+ sha256: string;
12
+ bytes: number;
13
+ };
14
+ }
15
+ export declare const nativeRuntimeFor: ({ platform, arch, musl, }?: {
16
+ platform?: string;
17
+ arch?: string;
18
+ musl?: boolean;
19
+ }) => NativeRuntime | undefined;
20
+ export declare const runtimeFolder: (runtime: NativeRuntime, env?: NodeJS.ProcessEnv) => string;
21
+ /**
22
+ * The library this system's binding links to, out of Microsoft's release archive, checked against its own sha256
23
+ * and placed beside a copy of the binding, which loads it from its own folder. Only from `models download`: a search
24
+ * never downloads. `undefined` when this system runs models through WebAssembly only.
25
+ */
26
+ export declare const installNativeRuntime: ({ env, progress, fetch, runtime, binding, }?: {
27
+ env?: NodeJS.ProcessEnv;
28
+ progress?: (line: string) => void;
29
+ fetch?: Fetch;
30
+ runtime?: NativeRuntime;
31
+ binding?: string;
32
+ }) => Promise<string | undefined>;
33
+ export type Ort = typeof import("@wirecat/cli-core-onnx");
34
+ export interface OnnxRuntime {
35
+ readonly ort: Ort;
36
+ readonly native: boolean;
37
+ }
38
+ /**
39
+ * The native runtime when `models download` placed it for this system and it loads, else WebAssembly. One per
40
+ * process: a binding, once loaded, stays.
41
+ */
42
+ export declare const onnxRuntime: (env?: NodeJS.ProcessEnv) => Promise<OnnxRuntime>;
@@ -0,0 +1,161 @@
1
+ import { createHash } from "node:crypto";
2
+ import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { CliError } from "@wirecat/cli-base";
5
+ import { gunzipSync } from "fflate";
6
+ import { installFiles, megabytes, sharedModelsDirectory } from "../speech/install.js";
7
+ export const ONNX_RUNTIME_VERSION = "1.30.0";
8
+ const release = (name, sha256, bytes) => ({
9
+ name,
10
+ url: `https://github.com/microsoft/onnxruntime/releases/download/v${ONNX_RUNTIME_VERSION}/${name}`,
11
+ sha256,
12
+ bytes,
13
+ });
14
+ /**
15
+ * The systems the ONNX workflow runs natively; everywhere else, and on musl, the models run through WebAssembly.
16
+ * Windows too: its binding needs the DirectML build of onnxruntime.dll, which Microsoft's release does not carry.
17
+ */
18
+ const NATIVE = {
19
+ "linux-x64": {
20
+ archive: release("onnxruntime-linux-x64-1.30.0.tgz", "a5ed5a3cac51fbb2e90da632ae43d19212faaa20e76484e62bcb7c23ddb3b3fd", 11_306_877),
21
+ member: "onnxruntime-linux-x64-1.30.0/lib/libonnxruntime.so.1.30.0",
22
+ library: {
23
+ name: "libonnxruntime.so.1",
24
+ sha256: "245a6f8c38127551057a1cd1ffd59f0a186a227ade4f3492dea2494eb565542e",
25
+ bytes: 28_985_152,
26
+ },
27
+ },
28
+ "linux-arm64": {
29
+ archive: release("onnxruntime-linux-aarch64-1.30.0.tgz", "e16a27a8ed330bbc698df7330b0cf56e722f354e3bcc92118682c74ef3c3e3da", 10_269_495),
30
+ member: "onnxruntime-linux-aarch64-1.30.0/lib/libonnxruntime.so.1.30.0",
31
+ library: {
32
+ name: "libonnxruntime.so.1",
33
+ sha256: "64e903a43a041240fd6bcffe0ac6d4fea47ef87bf24b9d097801bd00a9612a4b",
34
+ bytes: 25_135_496,
35
+ },
36
+ },
37
+ "darwin-arm64": {
38
+ archive: release("onnxruntime-osx-arm64-1.30.0.tgz", "6ebb5062a934537c352937821f9fe9718e7de1a2db1122a93dd363ffd53a7012", 42_373_116),
39
+ member: "onnxruntime-osx-arm64-1.30.0/lib/libonnxruntime.1.30.0.dylib",
40
+ library: {
41
+ name: "libonnxruntime.1.dylib",
42
+ sha256: "bcc9110f9d638a119de2db7afb3ba9a1da8085f0cb3401e1c48ae1caf450b6fa",
43
+ bytes: 43_879_424,
44
+ },
45
+ },
46
+ };
47
+ const onMusl = () => {
48
+ try {
49
+ return readFileSync("/proc/self/maps", "utf8").includes("ld-musl");
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ };
55
+ export const nativeRuntimeFor = ({ platform = process.platform, arch = process.arch, musl = platform === "linux" && onMusl(), } = {}) => {
56
+ const target = `${platform}-${arch}`;
57
+ const runtime = NATIVE[target];
58
+ return runtime && !musl ? { target, ...runtime } : undefined;
59
+ };
60
+ export const runtimeFolder = (runtime, env = process.env) => join(sharedModelsDirectory("runtime", env), `onnxruntime-${ONNX_RUNTIME_VERSION}-${runtime.target}`);
61
+ const BINDING = "onnxruntime_binding.node";
62
+ const sizeOf = (path) => {
63
+ try {
64
+ return statSync(path).size;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ };
70
+ const isInstalled = (runtime, folder) => sizeOf(join(folder, runtime.library.name)) === runtime.library.bytes && sizeOf(join(folder, BINDING)) !== undefined;
71
+ /**
72
+ * The library this system's binding links to, out of Microsoft's release archive, checked against its own sha256
73
+ * and placed beside a copy of the binding, which loads it from its own folder. Only from `models download`: a search
74
+ * never downloads. `undefined` when this system runs models through WebAssembly only.
75
+ */
76
+ export const installNativeRuntime = async ({ env = process.env, progress = () => { }, fetch, runtime = nativeRuntimeFor(), binding, } = {}) => {
77
+ if (!runtime)
78
+ return undefined;
79
+ const source = binding ?? (await import("@wirecat/cli-core-onnx")).nativeBindingFor();
80
+ if (!source)
81
+ return undefined;
82
+ const folder = runtimeFolder(runtime, env);
83
+ if (isInstalled(runtime, folder))
84
+ return folder;
85
+ progress(`ONNX Runtime ${ONNX_RUNTIME_VERSION} for ${runtime.target}: ${megabytes(runtime.archive.bytes)} from Microsoft`);
86
+ const parent = sharedModelsDirectory("runtime", env);
87
+ mkdirSync(parent, { recursive: true, mode: 0o700 });
88
+ const staging = mkdtempSync(join(parent, ".onnxruntime-"));
89
+ try {
90
+ const archive = join(staging, runtime.archive.name);
91
+ await installFiles([[runtime.archive, archive]], { ...(fetch ? { fetch } : {}), progress });
92
+ const library = tarMember(gunzipSync(readFileSync(archive)), runtime.member);
93
+ rmSync(archive);
94
+ const sha256 = library && createHash("sha256").update(library).digest("hex");
95
+ if (!library || library.length !== runtime.library.bytes || sha256 !== runtime.library.sha256)
96
+ throw new CliError("invalid_response", `${runtime.archive.name} does not hold the ${runtime.library.name} this version expects — nothing was installed`);
97
+ writeFileSync(join(staging, runtime.library.name), library, { mode: 0o755 });
98
+ copyFileSync(source, join(staging, BINDING));
99
+ rmSync(folder, { recursive: true, force: true });
100
+ renameSync(staging, folder);
101
+ return folder;
102
+ }
103
+ finally {
104
+ rmSync(staging, { recursive: true, force: true });
105
+ }
106
+ };
107
+ /** One regular file of a ustar archive, by its path: PAX and GNU long names, and a leading `./`, are understood. */
108
+ const tarMember = (tar, member) => {
109
+ const text = (from, length) => new TextDecoder().decode(tar.subarray(from, from + length)).replace(/\0.*$/s, "");
110
+ const plain = (path) => path.replace(/^\.\//, "");
111
+ let longName;
112
+ for (let offset = 0; offset + 512 <= tar.length;) {
113
+ const name = text(offset, 100);
114
+ if (!name)
115
+ return undefined;
116
+ const size = Number.parseInt(text(offset + 124, 12).trim() || "0", 8);
117
+ const type = text(offset + 156, 1);
118
+ const prefix = text(offset + 345, 155);
119
+ const data = tar.subarray(offset + 512, offset + 512 + size);
120
+ offset += 512 + Math.ceil(size / 512) * 512;
121
+ if (type === "x") {
122
+ longName = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(new TextDecoder().decode(data))?.[1] ?? longName;
123
+ continue;
124
+ }
125
+ if (type === "L") {
126
+ longName = new TextDecoder().decode(data).replace(/\0.*$/s, "");
127
+ continue;
128
+ }
129
+ const path = plain(longName ?? (prefix ? `${prefix}/${name}` : name));
130
+ longName = undefined;
131
+ if (path === plain(member) && (type === "0" || type === ""))
132
+ return data;
133
+ }
134
+ return undefined;
135
+ };
136
+ const opened = new Map();
137
+ /**
138
+ * The native runtime when `models download` placed it for this system and it loads, else WebAssembly. One per
139
+ * process: a binding, once loaded, stays.
140
+ */
141
+ export const onnxRuntime = (env = process.env) => {
142
+ const runtime = nativeRuntimeFor();
143
+ const folder = runtime ? runtimeFolder(runtime, env) : "";
144
+ let found = opened.get(folder);
145
+ if (!found) {
146
+ found = (async () => {
147
+ const wasm = await import("@wirecat/cli-core-onnx");
148
+ if (runtime && isInstalled(runtime, folder)) {
149
+ try {
150
+ return { ort: wasm.openNative(join(folder, BINDING)), native: true };
151
+ }
152
+ catch {
153
+ // A library that does not load on this system: WebAssembly runs the same models, slower.
154
+ }
155
+ }
156
+ return { ort: wasm, native: false };
157
+ })();
158
+ opened.set(folder, found);
159
+ }
160
+ return found;
161
+ };
@@ -1,9 +1,22 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { isTextModelInstalled, placedText, textModelsDirectory } from "../embeddings/embed.js";
3
3
  import { rerankModel, textModel } from "../embeddings/models.js";
4
+ import { installNativeRuntime } from "../embeddings/onnx-runtime.js";
4
5
  import { imageModel, imageModelsDirectory, isImageModelInstalled, placedImage, } from "../images/picture/models.js";
5
6
  import { installFiles, megabytes } from "../speech/install.js";
6
7
  const bytesOf = (model) => model.files.reduce((sum, file) => sum + file.bytes, 0);
8
+ /**
9
+ * The native ONNX Runtime beside the text models, where this system has one. It only makes them faster, so a
10
+ * failed download says why and the model is still installed, to run through WebAssembly.
11
+ */
12
+ const nativeRuntime = async ({ env, progress = () => { } }) => {
13
+ try {
14
+ await installNativeRuntime({ env, progress });
15
+ }
16
+ catch (error) {
17
+ progress(`ONNX Runtime not installed (${error instanceof Error ? error.message : String(error)}); models run through WebAssembly`);
18
+ }
19
+ };
7
20
  /**
8
21
  * The text embedding model into the shared models folder, checked against the sha256 this version expects and
9
22
  * loaded once on a word. Nothing is downloaded when it is already there; it is still loaded.
@@ -18,6 +31,7 @@ export const downloadTextModel = async (id, { env, progress = () => { }, acceptT
18
31
  progress(`${model.id}: ${megabytes(bytesOf(model))} from Hugging Face`);
19
32
  await installFiles(placedText(model, directory), { progress });
20
33
  }
34
+ await nativeRuntime({ env, progress });
21
35
  progress(`${model.id}: loading it once to check it works`);
22
36
  const { openEmbedder } = await import("../embeddings/embed.js");
23
37
  const embedder = await openEmbedder(model, directory);
@@ -39,6 +53,7 @@ export const downloadRerankModel = async (id, { env, progress = () => { } }) =>
39
53
  progress(`${model.id}: ${megabytes(bytesOf(model))} from Hugging Face`);
40
54
  await installFiles(placedText(model, directory), { progress });
41
55
  }
56
+ await nativeRuntime({ env, progress });
42
57
  progress(`${model.id}: loading it once to check it works`);
43
58
  const { openReranker } = await import("../embeddings/rerank.js");
44
59
  const reranker = await openReranker(model, directory);
@@ -1,6 +1,6 @@
1
1
  import { type ModelFile, type SpeechModel } from "./models.js";
2
2
  /** Shared by every CLI of the family, whatever it talks to: a model is hundreds of MB and is downloaded once. */
3
- export declare const sharedModelsDirectory: (kind: "audio" | "text" | "image", env?: NodeJS.ProcessEnv) => string;
3
+ export declare const sharedModelsDirectory: (kind: "audio" | "text" | "image" | "runtime", env?: NodeJS.ProcessEnv) => string;
4
4
  export declare const modelsDirectory: (env?: NodeJS.ProcessEnv) => string;
5
5
  export declare const modelPath: (directory: string, model: SpeechModel) => (name: string) => string;
6
6
  export declare const vadPath: (directory: string) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.447.0",
3
+ "version": "0.449.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",
@@ -176,7 +176,7 @@
176
176
  "@modelcontextprotocol/server": "^2.4.0",
177
177
  "@resvg/resvg-js": "2.6.2",
178
178
  "@valibot/to-json-schema": "1.8.0",
179
- "@wirecat/cli-core-onnx": "1.0.1",
179
+ "@wirecat/cli-core-onnx": "1.1.0",
180
180
  "@wirecat/cli-core-sqlite": "1.1.0",
181
181
  "@xmldom/xmldom": "0.9.12",
182
182
  "chardet": "2.2.0",
@@ -217,7 +217,7 @@
217
217
  "@types/node": "^22.20.5",
218
218
  "@types/pngjs": "6.0.5",
219
219
  "@vitest/coverage-v8": "^5.0.5",
220
- "@wirecat/cli-base": "0.59.0",
220
+ "@wirecat/cli-base": "0.62.0",
221
221
  "@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
222
222
  "commander": "15.0.0",
223
223
  "cspell": "10.4.0",
@@ -237,7 +237,7 @@
237
237
  },
238
238
  "peerDependencies": {
239
239
  "@napi-rs/canvas": ">=0.1.69 <2",
240
- "@wirecat/cli-base": ">=0.59.0 <0.60.0",
240
+ "@wirecat/cli-base": ">=0.62.0 <0.63.0",
241
241
  "commander": ">=15"
242
242
  },
243
243
  "bin": {