tryharness 0.7.23 → 0.7.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/assets/opencode-plugin.js +48 -11
  2. package/assets/pi-extension.ts +105 -26
  3. package/cli.mjs +1238 -322
  4. package/lib/activity.mjs +197 -40
  5. package/lib/adapters.mjs +337 -54
  6. package/lib/agentnode.mjs +175 -35
  7. package/lib/atlas.mjs +383 -83
  8. package/lib/cards.mjs +174 -40
  9. package/lib/claudemem.mjs +74 -18
  10. package/lib/commands.mjs +13 -5
  11. package/lib/context.mjs +409 -71
  12. package/lib/control-client.mjs +5 -2
  13. package/lib/coordinator.mjs +274 -57
  14. package/lib/daemon.mjs +5248 -1024
  15. package/lib/desktop.mjs +149 -35
  16. package/lib/digest.mjs +54 -13
  17. package/lib/environment.mjs +103 -21
  18. package/lib/files.mjs +246 -13
  19. package/lib/firewall.mjs +169 -35
  20. package/lib/folders.mjs +179 -36
  21. package/lib/frames.mjs +214 -40
  22. package/lib/inference.mjs +463 -126
  23. package/lib/learn.mjs +78 -16
  24. package/lib/live.mjs +286 -61
  25. package/lib/localmodels.mjs +424 -83
  26. package/lib/mcp-stdio.mjs +868 -151
  27. package/lib/media.mjs +138 -28
  28. package/lib/members.json +1 -1
  29. package/lib/members.mjs +35 -9
  30. package/lib/models.mjs +342 -80
  31. package/lib/network.mjs +201 -42
  32. package/lib/notify.mjs +49 -12
  33. package/lib/nudge.mjs +184 -33
  34. package/lib/ocr.mjs +223 -60
  35. package/lib/onboard.mjs +321 -70
  36. package/lib/parsers.mjs +132 -41
  37. package/lib/paths.mjs +33 -10
  38. package/lib/personal.mjs +51 -13
  39. package/lib/phones.mjs +382 -60
  40. package/lib/places.mjs +555 -107
  41. package/lib/provenance.mjs +19 -4
  42. package/lib/receipts.mjs +51 -11
  43. package/lib/redact.mjs +512 -116
  44. package/lib/runner.mjs +345 -55
  45. package/lib/runtime-install.mjs +55 -14
  46. package/lib/screens.mjs +154 -38
  47. package/lib/sealed.mjs +157 -28
  48. package/lib/secrets.mjs +18 -5
  49. package/lib/service.mjs +170 -34
  50. package/lib/snippets.mjs +94 -18
  51. package/lib/speech.mjs +688 -175
  52. package/lib/store.mjs +1654 -307
  53. package/lib/swarm.mjs +596 -117
  54. package/lib/t3.mjs +400 -79
  55. package/lib/trails.mjs +285 -61
  56. package/lib/ui.mjs +95 -23
  57. package/lib/vectors.mjs +5 -2
  58. package/lib/wakes.mjs +284 -68
  59. package/lib/watches.mjs +247 -48
  60. package/lib/worktrees.mjs +186 -38
  61. package/package.json +3 -3
@@ -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(["apache-2.0", "mit", "bsd-3-clause", "cc-by-4.0"]);
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": { name: "llama-b11609-bin-macos-arm64.tar.gz", sha256: "d03f28b5a5baa9cdfbb01db5ed22c695126c11c193326f14cdc51d001be10f4e", size: 12117158 },
21
- "darwin-x64": { name: "llama-b11609-bin-macos-x64.tar.gz", sha256: "f95aa88329eb017213114c5f09841e9ee02477dacca2652f2451d322464b4acb", size: 11614045 },
22
- "linux-x64-vulkan": { name: "llama-b11609-bin-ubuntu-vulkan-x64.tar.gz", sha256: "8aff188828de76492343e19905f72408c10f5b870a03592155b0e7dd19b76ed0", size: 30359149 },
23
- "linux-x64": { name: "llama-b11609-bin-ubuntu-x64.tar.gz", sha256: "f95cbb27dd6e377f2ce30f5cfd7ec987af3259c025fc7c98fff62927a16ff528", size: 17732148 },
24
- "linux-arm64-vulkan": { name: "llama-b11609-bin-ubuntu-vulkan-arm64.tar.gz", sha256: "27e37fc8cd9cfa00f99f72c5d4753a81ecbbce30f00266aa9338e5d7ac4dd9fb", size: 24120662 },
25
- "linux-arm64": { name: "llama-b11609-bin-ubuntu-arm64.tar.gz", sha256: "af83fe40920dc788bb96b267aa579e76c20ee90f7e777b4a7a3031a4fcb9d942", size: 13842753 },
26
- "win32-x64-vulkan": { name: "llama-b11609-bin-win-vulkan-x64.zip", sha256: "7b50bdb2a27b5fe17759104b1d86e082a05583ff1bca8b3202bcfb2a7232a005", size: 32134342 },
27
- "win32-x64": { name: "llama-b11609-bin-win-cpu-x64.zip", sha256: "8708a7b81842333927f6377ac3f17ff8bba4dbe1ee0cf28dfd5e7a32e3a46b38", size: 19518618 },
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", name: "EmbeddingGemma 2 (text)", purpose: "One search space over code, agent sessions and board posts",
34
- tier: "core", runtime: "llama", licence: "apache-2.0", repo: "ggml-org/embeddinggemma-2-GGUF", revision: "bfcd298762cc34d0357ece5ebdd31791a3a374d8",
35
- files: [{ path: "embeddinggemma-2-Q8_0.gguf", sha256: "2188ac1deca4b77dffefd603c2776a9d76d9d74ec01841392982ebb840b09135", size: 309855456 }],
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", name: "Wolf Defender small", purpose: "Spots instructions hidden in what one agent hands another",
40
- tier: "core", runtime: "onnx", licence: "apache-2.0", repo: "patronus-studio/wolf-defender-prompt-injection-small", revision: "bcab2eff97bcabd7227849639e2d0d7a61b46c92",
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
- { path: "onnx/int8_int4_embeddings/model.onnx", sha256: "a6c77496152e458c072e4787f872192af0449a427359d1afbfa1d4d6b116a305", size: 96296126 },
43
- { path: "onnx/onnx_fp16/tokenizer.json", sha256: "7e426c3929b44e6ab4c931770b5f22b913280633f5a1c67c81e9ad64decef55c", size: 34363287 },
44
- { path: "onnx/onnx_fp16/tokenizer_config.json", sha256: "14b147f2a4f939d9b12ab36e9633917040dd948fa78ce283b03402e4cf2c9cba", size: 574 },
45
- { path: "config.json", sha256: "b5bfba7b100b4b1aa361e8160e5593695164d81ca09b47f3b26561332b520218", size: 2084 },
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", name: "Horizon PI guard small", purpose: "Finds one planted instruction in a long page or post",
53
- tier: "core", runtime: "onnx", licence: "apache-2.0", repo: "Horizon-Labs/prompt-injection-guard-small", revision: "e0e88c4e940ee7e1b52bdf943d501ec696b83b9b",
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
- { path: "onnx/model_quantized.onnx", sha256: "61cd0741cdbfcc229e32e1de1392f352736d87d1e8b3c2b67ad9836bb7910d5d", size: 268409234 },
56
- { path: "config.json", sha256: "d6264037c9aa4b3d821c1c9a0329572033adbdb622788ecca91c3636066d9622", size: 2031 },
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", name: "credential-line 32M", purpose: "Spots keys and passwords before a brief leaves the computer",
62
- tier: "core", runtime: "onnx", licence: "mit", repo: "zaindanaharper/credential-line-32m", revision: "65422b823e9fb91d528b98229ebef6895d81de3b",
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
- { path: "onnx/model.onnx", sha256: "bd08c443d1472304624ea0b16ef8e08685a9e8fbe34ddfb9a65cfd00b6b4c47e", size: 128379418 },
65
- { path: "tokenizer.json", sha256: "218484396a9d08293b108b0e5ea31e0a1b7c801dcbed35a821deb392d2fe9bb4", size: 3583485 },
66
- { path: "tokenizer_config.json", sha256: "5bed9c3a3a8d5192d9e304ef695d265446452ba1b9a49e35183b74f982e21a53", size: 396 },
67
- { path: "config.json", sha256: "1fb928ec87f399096fdd444914c15a6643c9a0c4a7a7fc17c1e20f8bd61f7ea6", size: 1918 },
68
- { path: "policy.json", sha256: "db74709c4c08dc5d20732ec34aea723194bfa347553e97e81240f896a8de9770", size: 754 },
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", name: "EmbeddingGemma 2 (images and audio)", purpose: "Finds the minute of a recording or the screenshot a request is about, in folders the person adds",
75
- tier: "extended", runtime: "llama", licence: "apache-2.0", repo: "ggml-org/embeddinggemma-2-GGUF", revision: "bfcd298762cc34d0357ece5ebdd31791a3a374d8",
76
- files: [{ path: "mmproj-embeddinggemma-2-Q8_0.gguf", sha256: "c4a8a52691ecef40618438928bdf9e68379b854e24166f292592353db0aab64f", size: 554821024 }],
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", name: "PP-OCR (v6 small detector, v5 English recogniser)", purpose: "Reads screenshots so keys and personal details are hidden before an image leaves the computer",
83
- tier: "core", runtime: "onnx", licence: "apache-2.0", repo: "PaddlePaddle/en_PP-OCRv5_mobile_rec_onnx", revision: "3fafbc3b5dcf93dd72add9f48368be8a3a2cd33b",
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
- { path: "det/inference.onnx", repo: "PaddlePaddle/PP-OCRv6_small_det_onnx", revision: "28fe5895c24fd108c19eb3e8479f4ab385fbfc62", from: "inference.onnx", sha256: "d73e0058b7a8086bbd57f3d10b8bcd4ff95363f67e06e2762b5e814fe9c9410e", size: 9880512 },
86
- { path: "rec/inference.onnx", from: "inference.onnx", sha256: "b5f833dfc5d0eb71da397b4efa06ebeee9b431b690a47d6af40d77d8eabc557f", size: 7848423 },
87
- { path: "rec/inference.yml", from: "inference.yml", sha256: "27e91d0582f40168aa218303c76e184bc78fa7a5d105aad0cfbad8458b441067", size: 3964 },
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", name: "Silero VAD 6.2", purpose: "Hears whether anyone spoke while the talk key was held",
94
- tier: "extended", runtime: "onnx", licence: "mit", repo: "istupakov/silero-vad-onnx", revision: "b3e3ee3cce4c11ceb63b1a0b229d916069c1ddf6",
95
- files: [{ path: "silero_vad_16k_op15.onnx", sha256: "7ed98ddbad84ccac4cd0aeb3099049280713df825c610a8ed34543318f1b2c49", size: 1289603 }],
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", name: "Parakeet TDT 0.6B v3 (8-bit)", purpose: "Turns what the person says into text for the buddy's composer",
103
- tier: "extended", runtime: "onnx", licence: "cc-by-4.0", repo: "istupakov/parakeet-tdt-0.6b-v3-onnx", revision: "8f23f0c03c8761650bdb5b40aaf3e40d2c15f1ce", engine: "parakeet", threads: 2,
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
- { path: "encoder-model.int8.onnx", sha256: "6139d2fa7e1b086097b277c7149725edbab89cc7c7ae64b23c741be4055aff09", size: 652183999 },
106
- { path: "decoder_joint-model.int8.onnx", sha256: "eea7483ee3d1a30375daedc8ed83e3960c91b098812127a0d99d1c8977667a70", size: 18202004 },
107
- { path: "nemo128.onnx", sha256: "a9fde1486ebfcc08f328d75ad4610c67835fea58c73ba57e3209a6f6cf019e9f", size: 139764 },
108
- { path: "vocab.txt", sha256: "d58544679ea4bc6ac563d1f545eb7d474bd6cfa467f0a6e2c1dc1c7d37e3c35d", size: 93939 },
109
- { path: "config.json", sha256: "666903c76b9798caf2c210afd4f6cd60b08a8dbf9800ec8d7a3bc0d2148ac466", size: 97 },
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) => list.find((entry) => entry.id === id) ?? null;
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) => String(text)
121
- .replace(/[\u{E0020}-\u{E007E}]/gu, (c) => String.fromCodePoint(c.codePointAt(0) - 0xe0000))
122
- .replace(/[\u{E0110}-\u{E016E}]/gu, (c) => String.fromCodePoint(c.codePointAt(0) - 0xe0110 + 0x20))
123
- .normalize("NFKC")
124
- .replace(/[\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, "");
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) => path.join(modelDir(root, entry), ...entry.files[index].path.split("/"));
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)) throw new Error(`${entry.name}: licence ${entry.licence} is not on the allowlist`);
136
- if (!/^[0-9a-f]{40}$/.test(entry.revision || "")) throw new Error(`${entry.name}: not pinned to a repository commit`);
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)) throw new Error(`${entry.name}: ${file.path} has no pinned hash and size`);
139
- if ((file.repo !== undefined || file.revision !== undefined) && !(/^[\w.-]+\/[\w.-]+$/.test(file.repo || "") && /^[0-9a-f]{40}$/.test(file.revision || ""))) throw new Error(`${entry.name}: ${file.path} names another repository without pinning it to a commit`);
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) => { try { return fs.statSync(file).size === size; } catch { return false; } };
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 { have = fs.statSync(partial).size; } catch {}
157
- const response = await fetchImpl(url, { headers: have > 0 ? { range: `bytes=${have}-` } : {}, redirect: "follow" });
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) throw new Error(`download of ${path.basename(target)} failed: HTTP ${response.status}`);
160
- const out = fs.createWriteStream(partial, { flags: response.status === 206 ? "a" : "w" });
161
- for await (const chunk of response.body) if (!out.write(chunk)) await new Promise((resolve) => out.once("drain", resolve));
162
- await new Promise((resolve, reject) => { out.end(); out.on("finish", resolve); out.on("error", reject); });
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 (actual !== expected.sha256 || fs.statSync(partial).size !== expected.size) {
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(`${path.basename(target)} did not match its pinned hash; it was deleted and nothing was installed`);
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({ root, ids, list = MODELS, fetchImpl = fetch, log = () => {} }) {
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(modelById(id, list) ?? (() => { throw new Error(`unknown local model ${id}`); })());
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(`https://huggingface.co/${file.repo ?? entry.repo}/resolve/${file.revision ?? entry.revision}/${file.from ?? file.path}`, target, file, { fetchImpl, log });
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) => ids.every((id) => { const entry = modelById(id, list); return entry && entry.files.every((file, index) => present(modelFile(root, entry, index), file.size)); });
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({ platform = process.platform, arch = process.arch, gpu = true } = {}) {
487
+ export function llamaAsset({
488
+ platform = process.platform,
489
+ arch = process.arch,
490
+ gpu = true,
491
+ } = {}) {
192
492
  const base = `${platform}-${arch}`;
193
- return LLAMA.assets[gpu && platform !== "darwin" ? `${base}-vulkan` : base] ?? LLAMA.assets[base] ?? null;
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 = process.platform === "win32" ? "llama-server.exe" : "llama-server";
200
- for (const candidate of [path.join(dir, name), path.join(dir, "build", "bin", name), path.join(dir, `llama-${LLAMA.build}`, name)]) if (fs.existsSync(candidate)) return candidate;
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({ root, gpu = true, fetchImpl = fetch, log = () => {} }) {
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) throw new Error(`no llama.cpp build is pinned for ${process.platform}-${process.arch}`);
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)) await download(`https://github.com/ggml-org/llama.cpp/releases/download/${LLAMA.build}/${asset.name}`, archive, asset, { fetchImpl, log });
211
- else if ((await sha256File(archive)) !== asset.sha256) { fs.rmSync(archive, { force: true }); throw new Error(`${asset.name} did not match its pinned hash; it was deleted`); }
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("tar", ["-xzf", archive, "-C", dir, "--strip-components", "1"], { encoding: "utf8" });
217
- if (unpacked.status !== 0) throw new Error(`unpacking ${asset.name} failed: ${(unpacked.stderr || unpacked.error?.message || "").trim()}`);
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 = (spawnSync(server, ["--version"], { encoding: "utf8" }).stderr || "").match(/error while loading shared libraries: ([^:\s]+)/)?.[1];
228
- if (missing) throw new Error(`the local model server needs ${missing}, which this computer does not have${missing.startsWith("libgomp") ? " (on Ubuntu or Debian: sudo apt install libgomp1)" : ""}`);
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
  }