tryharness 0.7.23 → 0.7.26
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/assets/opencode-plugin.js +48 -11
- package/assets/pi-extension.ts +105 -26
- package/cli.mjs +1238 -322
- package/lib/activity.mjs +197 -40
- package/lib/adapters.mjs +337 -54
- package/lib/agentnode.mjs +175 -35
- package/lib/atlas.mjs +383 -83
- package/lib/cards.mjs +174 -40
- package/lib/claudemem.mjs +74 -18
- package/lib/commands.mjs +13 -5
- package/lib/context.mjs +409 -71
- package/lib/control-client.mjs +5 -2
- package/lib/coordinator.mjs +274 -57
- package/lib/daemon.mjs +5268 -1024
- package/lib/desktop.mjs +149 -35
- package/lib/digest.mjs +54 -13
- package/lib/environment.mjs +103 -21
- package/lib/files.mjs +246 -13
- package/lib/firewall.mjs +169 -35
- package/lib/folders.mjs +179 -36
- package/lib/frames.mjs +214 -40
- package/lib/inference.mjs +463 -126
- package/lib/learn.mjs +78 -16
- package/lib/live.mjs +286 -61
- package/lib/localmodels.mjs +424 -83
- package/lib/mcp-stdio.mjs +868 -151
- package/lib/media.mjs +138 -28
- package/lib/members.json +1 -1
- package/lib/members.mjs +35 -9
- package/lib/models.mjs +342 -80
- package/lib/network.mjs +201 -42
- package/lib/notify.mjs +49 -12
- package/lib/nudge.mjs +184 -33
- package/lib/ocr.mjs +223 -60
- package/lib/onboard.mjs +321 -70
- package/lib/parsers.mjs +132 -41
- package/lib/paths.mjs +33 -10
- package/lib/personal.mjs +51 -13
- package/lib/phones.mjs +384 -60
- package/lib/places.mjs +555 -107
- package/lib/provenance.mjs +19 -4
- package/lib/receipts.mjs +51 -11
- package/lib/redact.mjs +512 -116
- package/lib/runner.mjs +345 -55
- package/lib/runtime-install.mjs +55 -14
- package/lib/screens.mjs +154 -38
- package/lib/sealed.mjs +157 -28
- package/lib/secrets.mjs +18 -5
- package/lib/service.mjs +170 -34
- package/lib/snippets.mjs +94 -18
- package/lib/speech.mjs +688 -175
- package/lib/store.mjs +1654 -307
- package/lib/swarm.mjs +596 -117
- package/lib/t3.mjs +400 -79
- package/lib/trails.mjs +285 -61
- package/lib/ui.mjs +95 -23
- package/lib/vectors.mjs +5 -2
- package/lib/wakes.mjs +284 -68
- package/lib/watches.mjs +247 -48
- package/lib/worktrees.mjs +186 -38
- package/package.json +3 -3
package/lib/localmodels.mjs
CHANGED
|
@@ -9,7 +9,12 @@ import { ensureDir } from "./files.mjs";
|
|
|
9
9
|
// file whose hash differs, and refuses any model whose licence is not on the allowlist. The same list drives what the
|
|
10
10
|
// person can see under "Local models". Core models come down with setup; extended ones the first time a feature needs them.
|
|
11
11
|
|
|
12
|
-
export const LICENCES = new Set([
|
|
12
|
+
export const LICENCES = new Set([
|
|
13
|
+
"apache-2.0",
|
|
14
|
+
"mit",
|
|
15
|
+
"bsd-3-clause",
|
|
16
|
+
"cc-by-4.0",
|
|
17
|
+
]);
|
|
13
18
|
|
|
14
19
|
// llama.cpp's own release build: the server that hosts every GGUF model (search, decision models) on the GPU when
|
|
15
20
|
// there is one. Release archives as published, pinned by the digest GitHub reports for each asset.
|
|
@@ -17,114 +22,337 @@ export const LLAMA = {
|
|
|
17
22
|
build: "b11609",
|
|
18
23
|
licence: "mit",
|
|
19
24
|
assets: {
|
|
20
|
-
"darwin-arm64": {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
25
|
+
"darwin-arm64": {
|
|
26
|
+
name: "llama-b11609-bin-macos-arm64.tar.gz",
|
|
27
|
+
sha256:
|
|
28
|
+
"d03f28b5a5baa9cdfbb01db5ed22c695126c11c193326f14cdc51d001be10f4e",
|
|
29
|
+
size: 12117158,
|
|
30
|
+
},
|
|
31
|
+
"darwin-x64": {
|
|
32
|
+
name: "llama-b11609-bin-macos-x64.tar.gz",
|
|
33
|
+
sha256:
|
|
34
|
+
"f95aa88329eb017213114c5f09841e9ee02477dacca2652f2451d322464b4acb",
|
|
35
|
+
size: 11614045,
|
|
36
|
+
},
|
|
37
|
+
"linux-x64-vulkan": {
|
|
38
|
+
name: "llama-b11609-bin-ubuntu-vulkan-x64.tar.gz",
|
|
39
|
+
sha256:
|
|
40
|
+
"8aff188828de76492343e19905f72408c10f5b870a03592155b0e7dd19b76ed0",
|
|
41
|
+
size: 30359149,
|
|
42
|
+
},
|
|
43
|
+
"linux-x64": {
|
|
44
|
+
name: "llama-b11609-bin-ubuntu-x64.tar.gz",
|
|
45
|
+
sha256:
|
|
46
|
+
"f95cbb27dd6e377f2ce30f5cfd7ec987af3259c025fc7c98fff62927a16ff528",
|
|
47
|
+
size: 17732148,
|
|
48
|
+
},
|
|
49
|
+
"linux-arm64-vulkan": {
|
|
50
|
+
name: "llama-b11609-bin-ubuntu-vulkan-arm64.tar.gz",
|
|
51
|
+
sha256:
|
|
52
|
+
"27e37fc8cd9cfa00f99f72c5d4753a81ecbbce30f00266aa9338e5d7ac4dd9fb",
|
|
53
|
+
size: 24120662,
|
|
54
|
+
},
|
|
55
|
+
"linux-arm64": {
|
|
56
|
+
name: "llama-b11609-bin-ubuntu-arm64.tar.gz",
|
|
57
|
+
sha256:
|
|
58
|
+
"af83fe40920dc788bb96b267aa579e76c20ee90f7e777b4a7a3031a4fcb9d942",
|
|
59
|
+
size: 13842753,
|
|
60
|
+
},
|
|
61
|
+
"win32-x64-vulkan": {
|
|
62
|
+
name: "llama-b11609-bin-win-vulkan-x64.zip",
|
|
63
|
+
sha256:
|
|
64
|
+
"7b50bdb2a27b5fe17759104b1d86e082a05583ff1bca8b3202bcfb2a7232a005",
|
|
65
|
+
size: 32134342,
|
|
66
|
+
},
|
|
67
|
+
"win32-x64": {
|
|
68
|
+
name: "llama-b11609-bin-win-cpu-x64.zip",
|
|
69
|
+
sha256:
|
|
70
|
+
"8708a7b81842333927f6377ac3f17ff8bba4dbe1ee0cf28dfd5e7a32e3a46b38",
|
|
71
|
+
size: 19518618,
|
|
72
|
+
},
|
|
28
73
|
},
|
|
29
74
|
};
|
|
30
75
|
|
|
31
76
|
export const MODELS = [
|
|
32
77
|
{
|
|
33
|
-
id: "search",
|
|
34
|
-
|
|
35
|
-
|
|
78
|
+
id: "search",
|
|
79
|
+
name: "EmbeddingGemma 2 (text)",
|
|
80
|
+
purpose: "One search space over code, agent sessions and board posts",
|
|
81
|
+
tier: "core",
|
|
82
|
+
runtime: "llama",
|
|
83
|
+
licence: "apache-2.0",
|
|
84
|
+
repo: "ggml-org/embeddinggemma-2-GGUF",
|
|
85
|
+
revision: "bfcd298762cc34d0357ece5ebdd31791a3a374d8",
|
|
86
|
+
files: [
|
|
87
|
+
{
|
|
88
|
+
path: "embeddinggemma-2-Q8_0.gguf",
|
|
89
|
+
sha256:
|
|
90
|
+
"2188ac1deca4b77dffefd603c2776a9d76d9d74ec01841392982ebb840b09135",
|
|
91
|
+
size: 309855456,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
36
94
|
llama: { embedding: true, c: 2048, ub: 2048, b: 2048 },
|
|
37
95
|
},
|
|
38
96
|
{
|
|
39
|
-
id: "guard",
|
|
40
|
-
|
|
97
|
+
id: "guard",
|
|
98
|
+
name: "Wolf Defender small",
|
|
99
|
+
purpose: "Spots instructions hidden in what one agent hands another",
|
|
100
|
+
tier: "core",
|
|
101
|
+
runtime: "onnx",
|
|
102
|
+
licence: "apache-2.0",
|
|
103
|
+
repo: "patronus-studio/wolf-defender-prompt-injection-small",
|
|
104
|
+
revision: "bcab2eff97bcabd7227849639e2d0d7a61b46c92",
|
|
41
105
|
files: [
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
106
|
+
{
|
|
107
|
+
path: "onnx/int8_int4_embeddings/model.onnx",
|
|
108
|
+
sha256:
|
|
109
|
+
"a6c77496152e458c072e4787f872192af0449a427359d1afbfa1d4d6b116a305",
|
|
110
|
+
size: 96296126,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
path: "onnx/onnx_fp16/tokenizer.json",
|
|
114
|
+
sha256:
|
|
115
|
+
"7e426c3929b44e6ab4c931770b5f22b913280633f5a1c67c81e9ad64decef55c",
|
|
116
|
+
size: 34363287,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
path: "onnx/onnx_fp16/tokenizer_config.json",
|
|
120
|
+
sha256:
|
|
121
|
+
"14b147f2a4f939d9b12ab36e9633917040dd948fa78ce283b03402e4cf2c9cba",
|
|
122
|
+
size: 574,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
path: "config.json",
|
|
126
|
+
sha256:
|
|
127
|
+
"b5bfba7b100b4b1aa361e8160e5593695164d81ca09b47f3b26561332b520218",
|
|
128
|
+
size: 2084,
|
|
129
|
+
},
|
|
46
130
|
],
|
|
47
131
|
},
|
|
48
132
|
// The second guard reads short windows: in a long page or post, one planted sentence moves a 300-character window
|
|
49
133
|
// more than an 1,800-character one, and this model separates those better than the first (handoff-firewall eval). It
|
|
50
134
|
// is built on the first guard's vocabulary, so it uses that tokenizer (about 450 MB in memory) after its own extra step.
|
|
51
135
|
{
|
|
52
|
-
id: "guard-short",
|
|
53
|
-
|
|
136
|
+
id: "guard-short",
|
|
137
|
+
name: "Horizon PI guard small",
|
|
138
|
+
purpose: "Finds one planted instruction in a long page or post",
|
|
139
|
+
tier: "core",
|
|
140
|
+
runtime: "onnx",
|
|
141
|
+
licence: "apache-2.0",
|
|
142
|
+
repo: "Horizon-Labs/prompt-injection-guard-small",
|
|
143
|
+
revision: "e0e88c4e940ee7e1b52bdf943d501ec696b83b9b",
|
|
54
144
|
files: [
|
|
55
|
-
{
|
|
56
|
-
|
|
145
|
+
{
|
|
146
|
+
path: "onnx/model_quantized.onnx",
|
|
147
|
+
sha256:
|
|
148
|
+
"61cd0741cdbfcc229e32e1de1392f352736d87d1e8b3c2b67ad9836bb7910d5d",
|
|
149
|
+
size: 268409234,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
path: "config.json",
|
|
153
|
+
sha256:
|
|
154
|
+
"d6264037c9aa4b3d821c1c9a0329572033adbdb622788ecca91c3636066d9622",
|
|
155
|
+
size: 2031,
|
|
156
|
+
},
|
|
57
157
|
],
|
|
58
158
|
tokenizer: { from: "guard", prepare: (text) => unhide(text) },
|
|
59
159
|
},
|
|
60
160
|
{
|
|
61
|
-
id: "credentials",
|
|
62
|
-
|
|
161
|
+
id: "credentials",
|
|
162
|
+
name: "credential-line 32M",
|
|
163
|
+
purpose: "Spots keys and passwords before a brief leaves the computer",
|
|
164
|
+
tier: "core",
|
|
165
|
+
runtime: "onnx",
|
|
166
|
+
licence: "mit",
|
|
167
|
+
repo: "zaindanaharper/credential-line-32m",
|
|
168
|
+
revision: "65422b823e9fb91d528b98229ebef6895d81de3b",
|
|
63
169
|
files: [
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
170
|
+
{
|
|
171
|
+
path: "onnx/model.onnx",
|
|
172
|
+
sha256:
|
|
173
|
+
"bd08c443d1472304624ea0b16ef8e08685a9e8fbe34ddfb9a65cfd00b6b4c47e",
|
|
174
|
+
size: 128379418,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: "tokenizer.json",
|
|
178
|
+
sha256:
|
|
179
|
+
"218484396a9d08293b108b0e5ea31e0a1b7c801dcbed35a821deb392d2fe9bb4",
|
|
180
|
+
size: 3583485,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
path: "tokenizer_config.json",
|
|
184
|
+
sha256:
|
|
185
|
+
"5bed9c3a3a8d5192d9e304ef695d265446452ba1b9a49e35183b74f982e21a53",
|
|
186
|
+
size: 396,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
path: "config.json",
|
|
190
|
+
sha256:
|
|
191
|
+
"1fb928ec87f399096fdd444914c15a6643c9a0c4a7a7fc17c1e20f8bd61f7ea6",
|
|
192
|
+
size: 1918,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
path: "policy.json",
|
|
196
|
+
sha256:
|
|
197
|
+
"db74709c4c08dc5d20732ec34aea723194bfa347553e97e81240f896a8de9770",
|
|
198
|
+
size: 754,
|
|
199
|
+
},
|
|
69
200
|
],
|
|
70
201
|
},
|
|
71
202
|
// The search model's image and audio part: the same weights as search with its projector beside them, so a recording
|
|
72
203
|
// or a screenshot lands in the same space as a typed question. Downloaded when the person first adds a folder of them.
|
|
73
204
|
{
|
|
74
|
-
id: "media",
|
|
75
|
-
|
|
76
|
-
|
|
205
|
+
id: "media",
|
|
206
|
+
name: "EmbeddingGemma 2 (images and audio)",
|
|
207
|
+
purpose:
|
|
208
|
+
"Finds the minute of a recording or the screenshot a request is about, in folders the person adds",
|
|
209
|
+
tier: "extended",
|
|
210
|
+
runtime: "llama",
|
|
211
|
+
licence: "apache-2.0",
|
|
212
|
+
repo: "ggml-org/embeddinggemma-2-GGUF",
|
|
213
|
+
revision: "bfcd298762cc34d0357ece5ebdd31791a3a374d8",
|
|
214
|
+
files: [
|
|
215
|
+
{
|
|
216
|
+
path: "mmproj-embeddinggemma-2-Q8_0.gguf",
|
|
217
|
+
sha256:
|
|
218
|
+
"c4a8a52691ecef40618438928bdf9e68379b854e24166f292592353db0aab64f",
|
|
219
|
+
size: 554821024,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
77
222
|
llama: { weights: "search", embedding: true, c: 2048, ub: 2048, b: 2048 },
|
|
78
223
|
},
|
|
79
224
|
// The browser's OCR pair (src/shared/localRuntime/gpuWorker/ocr): PaddlePaddle's own ONNX exports, the detector byte
|
|
80
225
|
// for byte the one the browser serves. The recogniser's character list is read from its inference.yml.
|
|
81
226
|
{
|
|
82
|
-
id: "ocr",
|
|
83
|
-
|
|
227
|
+
id: "ocr",
|
|
228
|
+
name: "PP-OCR (v6 small detector, v5 English recogniser)",
|
|
229
|
+
purpose:
|
|
230
|
+
"Reads screenshots so keys and personal details are hidden before an image leaves the computer",
|
|
231
|
+
tier: "core",
|
|
232
|
+
runtime: "onnx",
|
|
233
|
+
licence: "apache-2.0",
|
|
234
|
+
repo: "PaddlePaddle/en_PP-OCRv5_mobile_rec_onnx",
|
|
235
|
+
revision: "3fafbc3b5dcf93dd72add9f48368be8a3a2cd33b",
|
|
84
236
|
files: [
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
237
|
+
{
|
|
238
|
+
path: "det/inference.onnx",
|
|
239
|
+
repo: "PaddlePaddle/PP-OCRv6_small_det_onnx",
|
|
240
|
+
revision: "28fe5895c24fd108c19eb3e8479f4ab385fbfc62",
|
|
241
|
+
from: "inference.onnx",
|
|
242
|
+
sha256:
|
|
243
|
+
"d73e0058b7a8086bbd57f3d10b8bcd4ff95363f67e06e2762b5e814fe9c9410e",
|
|
244
|
+
size: 9880512,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
path: "rec/inference.onnx",
|
|
248
|
+
from: "inference.onnx",
|
|
249
|
+
sha256:
|
|
250
|
+
"b5f833dfc5d0eb71da397b4efa06ebeee9b431b690a47d6af40d77d8eabc557f",
|
|
251
|
+
size: 7848423,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
path: "rec/inference.yml",
|
|
255
|
+
from: "inference.yml",
|
|
256
|
+
sha256:
|
|
257
|
+
"27e91d0582f40168aa218303c76e184bc78fa7a5d105aad0cfbad8458b441067",
|
|
258
|
+
size: 3964,
|
|
259
|
+
},
|
|
88
260
|
],
|
|
89
261
|
},
|
|
90
262
|
// Talk in the buddy (speech.mjs), downloaded the first time the person holds it: the VAD always, the speech model only
|
|
91
263
|
// where Apple's transcriber is not used.
|
|
92
264
|
{
|
|
93
|
-
id: "vad",
|
|
94
|
-
|
|
95
|
-
|
|
265
|
+
id: "vad",
|
|
266
|
+
name: "Silero VAD 6.2",
|
|
267
|
+
purpose: "Hears whether anyone spoke while the talk key was held",
|
|
268
|
+
tier: "extended",
|
|
269
|
+
runtime: "onnx",
|
|
270
|
+
licence: "mit",
|
|
271
|
+
repo: "istupakov/silero-vad-onnx",
|
|
272
|
+
revision: "b3e3ee3cce4c11ceb63b1a0b229d916069c1ddf6",
|
|
273
|
+
files: [
|
|
274
|
+
{
|
|
275
|
+
path: "silero_vad_16k_op15.onnx",
|
|
276
|
+
sha256:
|
|
277
|
+
"7ed98ddbad84ccac4cd0aeb3099049280713df825c610a8ed34543318f1b2c49",
|
|
278
|
+
size: 1289603,
|
|
279
|
+
},
|
|
280
|
+
],
|
|
96
281
|
},
|
|
97
282
|
// Speech where Apple's own transcriber is not on the computer (Linux, Windows, Macs before macOS 26): NVIDIA's
|
|
98
283
|
// Parakeet TDT 0.6B v3 (CC-BY-4.0; credited in the CLI's README), the 8-bit ONNX export by istupakov. Measured in the
|
|
99
284
|
// dictation eval against the Moonshine base it replaced: 2.8% of words wrong on real people reading against 4.9%. Two
|
|
100
285
|
// threads: a 5 s recording in 130 ms here against 205 ms on one (four: 99 ms).
|
|
101
286
|
{
|
|
102
|
-
id: "speech",
|
|
103
|
-
|
|
287
|
+
id: "speech",
|
|
288
|
+
name: "Parakeet TDT 0.6B v3 (8-bit)",
|
|
289
|
+
purpose: "Turns what the person says into text for the buddy's composer",
|
|
290
|
+
tier: "extended",
|
|
291
|
+
runtime: "onnx",
|
|
292
|
+
licence: "cc-by-4.0",
|
|
293
|
+
repo: "istupakov/parakeet-tdt-0.6b-v3-onnx",
|
|
294
|
+
revision: "8f23f0c03c8761650bdb5b40aaf3e40d2c15f1ce",
|
|
295
|
+
engine: "parakeet",
|
|
296
|
+
threads: 2,
|
|
104
297
|
files: [
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
298
|
+
{
|
|
299
|
+
path: "encoder-model.int8.onnx",
|
|
300
|
+
sha256:
|
|
301
|
+
"6139d2fa7e1b086097b277c7149725edbab89cc7c7ae64b23c741be4055aff09",
|
|
302
|
+
size: 652183999,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
path: "decoder_joint-model.int8.onnx",
|
|
306
|
+
sha256:
|
|
307
|
+
"eea7483ee3d1a30375daedc8ed83e3960c91b098812127a0d99d1c8977667a70",
|
|
308
|
+
size: 18202004,
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
path: "nemo128.onnx",
|
|
312
|
+
sha256:
|
|
313
|
+
"a9fde1486ebfcc08f328d75ad4610c67835fea58c73ba57e3209a6f6cf019e9f",
|
|
314
|
+
size: 139764,
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
path: "vocab.txt",
|
|
318
|
+
sha256:
|
|
319
|
+
"d58544679ea4bc6ac563d1f545eb7d474bd6cfa467f0a6e2c1dc1c7d37e3c35d",
|
|
320
|
+
size: 93939,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
path: "config.json",
|
|
324
|
+
sha256:
|
|
325
|
+
"666903c76b9798caf2c210afd4f6cd60b08a8dbf9800ec8d7a3bc0d2148ac466",
|
|
326
|
+
size: 97,
|
|
327
|
+
},
|
|
110
328
|
],
|
|
111
329
|
},
|
|
112
330
|
];
|
|
113
331
|
|
|
114
|
-
export const modelById = (id, list = MODELS) =>
|
|
332
|
+
export const modelById = (id, list = MODELS) =>
|
|
333
|
+
list.find((entry) => entry.id === id) ?? null;
|
|
115
334
|
/**
|
|
116
335
|
* The extra step in Horizon PI guard small's tokenizer before the one it shares with Wolf Defender (same token ids on
|
|
117
336
|
* every text tried, hidden ones included): letters spelled in Unicode tag characters or variation selectors become
|
|
118
337
|
* plain letters, NFKC, then invisible and overlay characters go.
|
|
119
338
|
*/
|
|
120
|
-
export const unhide = (text) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
339
|
+
export const unhide = (text) =>
|
|
340
|
+
String(text)
|
|
341
|
+
.replace(/[\u{E0020}-\u{E007E}]/gu, (c) =>
|
|
342
|
+
String.fromCodePoint(c.codePointAt(0) - 0xe0000)
|
|
343
|
+
)
|
|
344
|
+
.replace(/[\u{E0110}-\u{E016E}]/gu, (c) =>
|
|
345
|
+
String.fromCodePoint(c.codePointAt(0) - 0xe0110 + 0x20)
|
|
346
|
+
)
|
|
347
|
+
.normalize("NFKC")
|
|
348
|
+
.replace(
|
|
349
|
+
/[\u00AD\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF\uFE00-\uFE0F\u{E0000}-\u{E007F}\u{E0100}-\u{E01EF}\u0305\u0331-\u0338\u20D0-\u20FF]/gu,
|
|
350
|
+
""
|
|
351
|
+
);
|
|
125
352
|
|
|
126
353
|
export const modelDir = (root, entry) => path.join(root, entry.id);
|
|
127
|
-
export const modelFile = (root, entry, index = 0) =>
|
|
354
|
+
export const modelFile = (root, entry, index = 0) =>
|
|
355
|
+
path.join(modelDir(root, entry), ...entry.files[index].path.split("/"));
|
|
128
356
|
|
|
129
357
|
/**
|
|
130
358
|
* Refuses an entry that could not be verified or used: unpinned, unhashed, or under a licence off the allowlist. A file
|
|
@@ -132,11 +360,27 @@ export const modelFile = (root, entry, index = 0) => path.join(modelDir(root, en
|
|
|
132
360
|
* (from), so models made of parts published apart stay one entry.
|
|
133
361
|
*/
|
|
134
362
|
export function checkEntry(entry) {
|
|
135
|
-
if (!LICENCES.has(entry.licence))
|
|
136
|
-
|
|
363
|
+
if (!LICENCES.has(entry.licence))
|
|
364
|
+
throw new Error(
|
|
365
|
+
`${entry.name}: licence ${entry.licence} is not on the allowlist`
|
|
366
|
+
);
|
|
367
|
+
if (!/^[0-9a-f]{40}$/.test(entry.revision || ""))
|
|
368
|
+
throw new Error(`${entry.name}: not pinned to a repository commit`);
|
|
137
369
|
for (const file of entry.files) {
|
|
138
|
-
if (!/^[0-9a-f]{64}$/.test(file.sha256 || "") || !(file.size > 0))
|
|
139
|
-
|
|
370
|
+
if (!/^[0-9a-f]{64}$/.test(file.sha256 || "") || !(file.size > 0))
|
|
371
|
+
throw new Error(
|
|
372
|
+
`${entry.name}: ${file.path} has no pinned hash and size`
|
|
373
|
+
);
|
|
374
|
+
if (
|
|
375
|
+
(file.repo !== undefined || file.revision !== undefined) &&
|
|
376
|
+
!(
|
|
377
|
+
/^[\w.-]+\/[\w.-]+$/.test(file.repo || "") &&
|
|
378
|
+
/^[0-9a-f]{40}$/.test(file.revision || "")
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
throw new Error(
|
|
382
|
+
`${entry.name}: ${file.path} names another repository without pinning it to a commit`
|
|
383
|
+
);
|
|
140
384
|
}
|
|
141
385
|
return entry;
|
|
142
386
|
}
|
|
@@ -147,74 +391,166 @@ export async function sha256File(file) {
|
|
|
147
391
|
return hash.digest("hex");
|
|
148
392
|
}
|
|
149
393
|
|
|
150
|
-
const present = (file, size) => {
|
|
394
|
+
const present = (file, size) => {
|
|
395
|
+
try {
|
|
396
|
+
return fs.statSync(file).size === size;
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
151
401
|
|
|
152
402
|
async function download(url, target, expected, { fetchImpl, log }) {
|
|
153
403
|
const partial = `${target}.partial`;
|
|
154
404
|
ensureDir(path.dirname(target));
|
|
155
405
|
let have = 0;
|
|
156
|
-
try {
|
|
157
|
-
|
|
406
|
+
try {
|
|
407
|
+
have = fs.statSync(partial).size;
|
|
408
|
+
} catch {}
|
|
409
|
+
const response = await fetchImpl(url, {
|
|
410
|
+
headers: have > 0 ? { range: `bytes=${have}-` } : {},
|
|
411
|
+
redirect: "follow",
|
|
412
|
+
});
|
|
158
413
|
if (response.status !== 416) {
|
|
159
|
-
if (!response.ok)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
414
|
+
if (!response.ok)
|
|
415
|
+
throw new Error(
|
|
416
|
+
`download of ${path.basename(target)} failed: HTTP ${response.status}`
|
|
417
|
+
);
|
|
418
|
+
const out = fs.createWriteStream(partial, {
|
|
419
|
+
flags: response.status === 206 ? "a" : "w",
|
|
420
|
+
});
|
|
421
|
+
for await (const chunk of response.body)
|
|
422
|
+
if (!out.write(chunk))
|
|
423
|
+
await new Promise((resolve) => out.once("drain", resolve));
|
|
424
|
+
await new Promise((resolve, reject) => {
|
|
425
|
+
out.end();
|
|
426
|
+
out.on("finish", resolve);
|
|
427
|
+
out.on("error", reject);
|
|
428
|
+
});
|
|
163
429
|
}
|
|
164
430
|
const actual = await sha256File(partial);
|
|
165
|
-
if (
|
|
431
|
+
if (
|
|
432
|
+
actual !== expected.sha256 ||
|
|
433
|
+
fs.statSync(partial).size !== expected.size
|
|
434
|
+
) {
|
|
166
435
|
fs.rmSync(partial, { force: true });
|
|
167
|
-
throw new Error(
|
|
436
|
+
throw new Error(
|
|
437
|
+
`${path.basename(target)} did not match its pinned hash; it was deleted and nothing was installed`
|
|
438
|
+
);
|
|
168
439
|
}
|
|
169
440
|
fs.renameSync(partial, target);
|
|
170
441
|
log?.(`local models: ${path.basename(target)} verified`);
|
|
171
442
|
}
|
|
172
443
|
|
|
173
444
|
/** Downloads and verifies the given models' missing files into root. Returns the ids now present. */
|
|
174
|
-
export async function ensureModels({
|
|
445
|
+
export async function ensureModels({
|
|
446
|
+
root,
|
|
447
|
+
ids,
|
|
448
|
+
list = MODELS,
|
|
449
|
+
fetchImpl = fetch,
|
|
450
|
+
log = () => {},
|
|
451
|
+
}) {
|
|
175
452
|
const done = [];
|
|
176
453
|
for (const id of ids) {
|
|
177
|
-
const entry = checkEntry(
|
|
454
|
+
const entry = checkEntry(
|
|
455
|
+
modelById(id, list) ??
|
|
456
|
+
(() => {
|
|
457
|
+
throw new Error(`unknown local model ${id}`);
|
|
458
|
+
})()
|
|
459
|
+
);
|
|
178
460
|
for (const [index, file] of entry.files.entries()) {
|
|
179
461
|
const target = modelFile(root, entry, index);
|
|
180
462
|
if (present(target, file.size)) continue;
|
|
181
|
-
await download(
|
|
463
|
+
await download(
|
|
464
|
+
`https://huggingface.co/${file.repo ?? entry.repo}/resolve/${file.revision ?? entry.revision}/${file.from ?? file.path}`,
|
|
465
|
+
target,
|
|
466
|
+
file,
|
|
467
|
+
{ fetchImpl, log }
|
|
468
|
+
);
|
|
182
469
|
}
|
|
183
470
|
done.push(id);
|
|
184
471
|
}
|
|
185
472
|
return done;
|
|
186
473
|
}
|
|
187
474
|
|
|
188
|
-
export const modelsPresent = (root, ids, list = MODELS) =>
|
|
475
|
+
export const modelsPresent = (root, ids, list = MODELS) =>
|
|
476
|
+
ids.every((id) => {
|
|
477
|
+
const entry = modelById(id, list);
|
|
478
|
+
return (
|
|
479
|
+
entry &&
|
|
480
|
+
entry.files.every((file, index) =>
|
|
481
|
+
present(modelFile(root, entry, index), file.size)
|
|
482
|
+
)
|
|
483
|
+
);
|
|
484
|
+
});
|
|
189
485
|
|
|
190
486
|
/** The llama.cpp build for this machine: the GPU build where the OS has one (Metal is built in on macOS). */
|
|
191
|
-
export function llamaAsset({
|
|
487
|
+
export function llamaAsset({
|
|
488
|
+
platform = process.platform,
|
|
489
|
+
arch = process.arch,
|
|
490
|
+
gpu = true,
|
|
491
|
+
} = {}) {
|
|
192
492
|
const base = `${platform}-${arch}`;
|
|
193
|
-
return
|
|
493
|
+
return (
|
|
494
|
+
LLAMA.assets[gpu && platform !== "darwin" ? `${base}-vulkan` : base] ??
|
|
495
|
+
LLAMA.assets[base] ??
|
|
496
|
+
null
|
|
497
|
+
);
|
|
194
498
|
}
|
|
195
499
|
|
|
196
500
|
export const llamaDir = (root) => path.join(root, `llama-${LLAMA.build}`);
|
|
197
501
|
export const llamaServer = (root) => {
|
|
198
502
|
const dir = llamaDir(root);
|
|
199
|
-
const name =
|
|
200
|
-
|
|
503
|
+
const name =
|
|
504
|
+
process.platform === "win32" ? "llama-server.exe" : "llama-server";
|
|
505
|
+
for (const candidate of [
|
|
506
|
+
path.join(dir, name),
|
|
507
|
+
path.join(dir, "build", "bin", name),
|
|
508
|
+
path.join(dir, `llama-${LLAMA.build}`, name),
|
|
509
|
+
])
|
|
510
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
201
511
|
return null;
|
|
202
512
|
};
|
|
203
513
|
|
|
204
514
|
/** Downloads, verifies and unpacks the llama.cpp server. Idempotent. */
|
|
205
|
-
export async function ensureLlama({
|
|
515
|
+
export async function ensureLlama({
|
|
516
|
+
root,
|
|
517
|
+
gpu = true,
|
|
518
|
+
fetchImpl = fetch,
|
|
519
|
+
log = () => {},
|
|
520
|
+
}) {
|
|
206
521
|
if (llamaServer(root)) return runs(llamaServer(root));
|
|
207
522
|
const asset = llamaAsset({ gpu });
|
|
208
|
-
if (!asset)
|
|
523
|
+
if (!asset)
|
|
524
|
+
throw new Error(
|
|
525
|
+
`no llama.cpp build is pinned for ${process.platform}-${process.arch}`
|
|
526
|
+
);
|
|
209
527
|
const archive = path.join(root, asset.name);
|
|
210
|
-
if (!present(archive, asset.size))
|
|
211
|
-
|
|
528
|
+
if (!present(archive, asset.size))
|
|
529
|
+
await download(
|
|
530
|
+
`https://github.com/ggml-org/llama.cpp/releases/download/${LLAMA.build}/${asset.name}`,
|
|
531
|
+
archive,
|
|
532
|
+
asset,
|
|
533
|
+
{ fetchImpl, log }
|
|
534
|
+
);
|
|
535
|
+
else if ((await sha256File(archive)) !== asset.sha256) {
|
|
536
|
+
fs.rmSync(archive, { force: true });
|
|
537
|
+
throw new Error(
|
|
538
|
+
`${asset.name} did not match its pinned hash; it was deleted`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
212
541
|
const dir = llamaDir(root);
|
|
213
542
|
ensureDir(dir);
|
|
214
543
|
const unpacked = asset.name.endsWith(".zip")
|
|
215
544
|
? spawnSync("tar", ["-xf", archive, "-C", dir], { encoding: "utf8" })
|
|
216
|
-
: spawnSync(
|
|
217
|
-
|
|
545
|
+
: spawnSync(
|
|
546
|
+
"tar",
|
|
547
|
+
["-xzf", archive, "-C", dir, "--strip-components", "1"],
|
|
548
|
+
{ encoding: "utf8" }
|
|
549
|
+
);
|
|
550
|
+
if (unpacked.status !== 0)
|
|
551
|
+
throw new Error(
|
|
552
|
+
`unpacking ${asset.name} failed: ${(unpacked.stderr || unpacked.error?.message || "").trim()}`
|
|
553
|
+
);
|
|
218
554
|
fs.rmSync(archive, { force: true });
|
|
219
555
|
const server = llamaServer(root);
|
|
220
556
|
if (!server) throw new Error(`${asset.name} has no llama-server in it`);
|
|
@@ -224,7 +560,12 @@ export async function ensureLlama({ root, gpu = true, fetchImpl = fetch, log = (
|
|
|
224
560
|
// A build that cannot start here says what is missing: on Linux, llama.cpp needs the system's OpenMP library, which
|
|
225
561
|
// minimal installs lack.
|
|
226
562
|
function runs(server) {
|
|
227
|
-
const missing = (
|
|
228
|
-
|
|
563
|
+
const missing = (
|
|
564
|
+
spawnSync(server, ["--version"], { encoding: "utf8" }).stderr || ""
|
|
565
|
+
).match(/error while loading shared libraries: ([^:\s]+)/)?.[1];
|
|
566
|
+
if (missing)
|
|
567
|
+
throw new Error(
|
|
568
|
+
`the local model server needs ${missing}, which this computer does not have${missing.startsWith("libgomp") ? " (on Ubuntu or Debian: sudo apt install libgomp1)" : ""}`
|
|
569
|
+
);
|
|
229
570
|
return server;
|
|
230
571
|
}
|