ruvector 0.2.28 → 0.2.30

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 (41) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2270 -2270
  3. package/bin/cli.js +9598 -9479
  4. package/bin/mcp-server.js +1 -1
  5. package/dist/core/intelligence-engine.d.ts +13 -0
  6. package/dist/core/intelligence-engine.d.ts.map +1 -1
  7. package/dist/core/intelligence-engine.js +38 -0
  8. package/dist/core/onnx/bundled-parallel.mjs +164 -164
  9. package/dist/core/onnx/embed-worker.mjs +67 -67
  10. package/dist/core/onnx/loader.js +434 -434
  11. package/dist/core/onnx/package.json +3 -3
  12. package/dist/core/onnx/pkg/LICENSE +21 -21
  13. package/dist/core/onnx/pkg/loader.js +348 -348
  14. package/dist/core/onnx/pkg/package.json +3 -3
  15. package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm.d.ts +112 -112
  16. package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm.js +5 -5
  17. package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm_bg.js +638 -638
  18. package/dist/core/onnx/pkg/ruvector_onnx_embeddings_wasm_bg.wasm.d.ts +29 -29
  19. package/dist/core/parallel-workers.js +439 -439
  20. package/dist/workers/benchmark.js +15 -15
  21. package/package.json +122 -122
  22. package/src/decompiler/api-prober.js +302 -302
  23. package/src/decompiler/index.js +463 -463
  24. package/src/decompiler/metrics.js +86 -86
  25. package/src/decompiler/model-decompiler.js +423 -423
  26. package/src/decompiler/module-splitter.js +498 -498
  27. package/src/decompiler/module-tree.js +142 -142
  28. package/src/decompiler/name-predictor.js +400 -400
  29. package/src/decompiler/npm-fetch.js +176 -176
  30. package/src/decompiler/reconstructor.js +499 -499
  31. package/src/decompiler/reference-tracker.js +285 -285
  32. package/src/decompiler/statement-parser.js +285 -285
  33. package/src/decompiler/style-improver.js +438 -438
  34. package/src/decompiler/subcategories.js +339 -339
  35. package/src/decompiler/validator.js +379 -379
  36. package/src/decompiler/witness.js +140 -140
  37. package/wasm/package.json +26 -26
  38. package/wasm/ruvector_decompiler_wasm.d.ts +27 -27
  39. package/wasm/ruvector_decompiler_wasm.js +220 -220
  40. package/wasm/ruvector_decompiler_wasm_bg.wasm.d.ts +16 -16
  41. package/dist/core/onnx/pkg/ruvector.db +0 -0
@@ -1,434 +1,434 @@
1
- /**
2
- * Model Loader for RuVector ONNX Embeddings WASM
3
- *
4
- * Provides easy loading of pre-trained models from HuggingFace Hub
5
- */
6
-
7
- /**
8
- * Pre-configured models with their HuggingFace URLs
9
- */
10
- export const MODELS = {
11
- // Sentence Transformers - Small & Fast
12
- 'all-MiniLM-L6-v2': {
13
- name: 'all-MiniLM-L6-v2',
14
- dimension: 384,
15
- maxLength: 256,
16
- size: '23MB',
17
- description: 'Fast, general-purpose embeddings',
18
- model: 'https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx',
19
- tokenizer: 'https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json',
20
- },
21
- 'all-MiniLM-L12-v2': {
22
- name: 'all-MiniLM-L12-v2',
23
- dimension: 384,
24
- maxLength: 256,
25
- size: '33MB',
26
- description: 'Better quality, balanced speed',
27
- model: 'https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2/resolve/main/onnx/model.onnx',
28
- tokenizer: 'https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2/resolve/main/tokenizer.json',
29
- },
30
-
31
- // BGE Models - State of the art
32
- 'bge-small-en-v1.5': {
33
- name: 'bge-small-en-v1.5',
34
- dimension: 384,
35
- maxLength: 512,
36
- size: '33MB',
37
- description: 'State-of-the-art small model',
38
- model: 'https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/onnx/model.onnx',
39
- tokenizer: 'https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/tokenizer.json',
40
- },
41
- 'bge-base-en-v1.5': {
42
- name: 'bge-base-en-v1.5',
43
- dimension: 768,
44
- maxLength: 512,
45
- size: '110MB',
46
- description: 'Best overall quality',
47
- model: 'https://huggingface.co/BAAI/bge-base-en-v1.5/resolve/main/onnx/model.onnx',
48
- tokenizer: 'https://huggingface.co/BAAI/bge-base-en-v1.5/resolve/main/tokenizer.json',
49
- },
50
-
51
- // E5 Models - Microsoft
52
- 'e5-small-v2': {
53
- name: 'e5-small-v2',
54
- dimension: 384,
55
- maxLength: 512,
56
- size: '33MB',
57
- description: 'Excellent for search & retrieval',
58
- model: 'https://huggingface.co/intfloat/e5-small-v2/resolve/main/onnx/model.onnx',
59
- tokenizer: 'https://huggingface.co/intfloat/e5-small-v2/resolve/main/tokenizer.json',
60
- },
61
-
62
- // GTE Models - Alibaba
63
- 'gte-small': {
64
- name: 'gte-small',
65
- dimension: 384,
66
- maxLength: 512,
67
- size: '33MB',
68
- description: 'Good multilingual support',
69
- model: 'https://huggingface.co/thenlper/gte-small/resolve/main/onnx/model.onnx',
70
- tokenizer: 'https://huggingface.co/thenlper/gte-small/resolve/main/tokenizer.json',
71
- },
72
- };
73
-
74
- /**
75
- * Default model for quick start
76
- */
77
- export const DEFAULT_MODEL = 'all-MiniLM-L6-v2';
78
-
79
- /**
80
- * In-memory memo of loaded models, keyed by model name. Deduplicates the
81
- * (re-)download + decode when multiple embedder instances load the same model
82
- * in one process. In Node there is no Cache API, so without this every
83
- * ModelLoader.loadModel() re-fetches the model from HuggingFace (issue #523).
84
- */
85
- const _inMemoryModelCache = new Map();
86
-
87
- /**
88
- * Model loader with caching support
89
- */
90
- export class ModelLoader {
91
- constructor(options = {}) {
92
- this.cache = options.cache ?? true;
93
- this.cacheStorage = options.cacheStorage ?? 'ruvector-models';
94
- this.onProgress = options.onProgress ?? null;
95
- }
96
-
97
- /**
98
- * Load a pre-configured model by name
99
- * @param {string} modelName - Model name from MODELS
100
- * @returns {Promise<{modelBytes: Uint8Array, tokenizerJson: string, config: object}>}
101
- */
102
- async loadModel(modelName = DEFAULT_MODEL) {
103
- const modelConfig = MODELS[modelName];
104
- if (!modelConfig) {
105
- throw new Error(`Unknown model: ${modelName}. Available: ${Object.keys(MODELS).join(', ')}`);
106
- }
107
-
108
- // In-memory memo: a second load of the same model in this process reuses
109
- // the already-downloaded bytes instead of re-fetching (issue #523).
110
- if (this.cache && _inMemoryModelCache.has(modelName)) {
111
- return _inMemoryModelCache.get(modelName);
112
- }
113
-
114
- // On-disk cache (Node only): models persist across processes so they are
115
- // downloaded once, not every run. The browser has the Cache API instead
116
- // (handled in fetchWithCache). See issue #523.
117
- if (this.cache) {
118
- const disk = await this._loadFromDisk(modelName);
119
- if (disk) {
120
- const cached = { ...disk, config: modelConfig };
121
- _inMemoryModelCache.set(modelName, cached);
122
- return cached;
123
- }
124
- }
125
-
126
- console.log(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
127
-
128
- const [modelBytes, tokenizerJson] = await Promise.all([
129
- this.fetchWithCache(modelConfig.model, `${modelName}-model.onnx`, 'arraybuffer'),
130
- this.fetchWithCache(modelConfig.tokenizer, `${modelName}-tokenizer.json`, 'text'),
131
- ]);
132
-
133
- const result = {
134
- modelBytes: new Uint8Array(modelBytes),
135
- tokenizerJson,
136
- config: modelConfig,
137
- };
138
-
139
- if (this.cache) {
140
- _inMemoryModelCache.set(modelName, result);
141
- await this._saveToDisk(modelName, result.modelBytes, tokenizerJson);
142
- }
143
-
144
- return result;
145
- }
146
-
147
- /**
148
- * Resolve the Node on-disk cache dir for a model (null in non-Node envs).
149
- * Uses dynamic import so this module stays loadable in browsers/bundlers.
150
- */
151
- async _diskCacheDir(modelName) {
152
- if (typeof process === 'undefined' || !process.versions?.node) return null;
153
- const home = process.env.RUVECTOR_CACHE_DIR
154
- || process.env.HOME || process.env.USERPROFILE || '/tmp';
155
- const path = await import('node:path');
156
- return path.join(home, '.ruvector', 'models', modelName);
157
- }
158
-
159
- /** Load model bytes + tokenizer from the Node disk cache, or null if absent. */
160
- async _loadFromDisk(modelName) {
161
- const dir = await this._diskCacheDir(modelName);
162
- if (!dir) return null;
163
- try {
164
- const fs = await import('node:fs');
165
- const path = await import('node:path');
166
- const modelPath = path.join(dir, 'model.onnx');
167
- const tokPath = path.join(dir, 'tokenizer.json');
168
- if (!fs.existsSync(modelPath) || !fs.existsSync(tokPath)) return null;
169
- const modelBytes = new Uint8Array(fs.readFileSync(modelPath));
170
- const tokenizerJson = fs.readFileSync(tokPath, 'utf8');
171
- if (modelBytes.length === 0 || tokenizerJson.length === 0) return null;
172
- console.log(` Disk cache hit: ${modelName}`);
173
- return { modelBytes, tokenizerJson };
174
- } catch {
175
- return null;
176
- }
177
- }
178
-
179
- /** Persist model bytes + tokenizer to the Node disk cache (best-effort). */
180
- async _saveToDisk(modelName, modelBytes, tokenizerJson) {
181
- const dir = await this._diskCacheDir(modelName);
182
- if (!dir) return;
183
- try {
184
- const fs = await import('node:fs');
185
- const path = await import('node:path');
186
- fs.mkdirSync(dir, { recursive: true });
187
- // Write to temp files then rename, so a crash mid-write can't leave a
188
- // truncated cache entry that later reads would trust.
189
- const mTmp = path.join(dir, 'model.onnx.tmp');
190
- const tTmp = path.join(dir, 'tokenizer.json.tmp');
191
- fs.writeFileSync(mTmp, Buffer.from(modelBytes));
192
- fs.writeFileSync(tTmp, tokenizerJson);
193
- fs.renameSync(mTmp, path.join(dir, 'model.onnx'));
194
- fs.renameSync(tTmp, path.join(dir, 'tokenizer.json'));
195
- } catch {
196
- // Cache write is best-effort; embedding still works without it.
197
- }
198
- }
199
-
200
- /**
201
- * Load model from custom URLs
202
- * @param {string} modelUrl - URL to ONNX model
203
- * @param {string} tokenizerUrl - URL to tokenizer.json
204
- * @returns {Promise<{modelBytes: Uint8Array, tokenizerJson: string}>}
205
- */
206
- async loadFromUrls(modelUrl, tokenizerUrl) {
207
- const [modelBytes, tokenizerJson] = await Promise.all([
208
- this.fetchWithCache(modelUrl, null, 'arraybuffer'),
209
- this.fetchWithCache(tokenizerUrl, null, 'text'),
210
- ]);
211
-
212
- return {
213
- modelBytes: new Uint8Array(modelBytes),
214
- tokenizerJson,
215
- };
216
- }
217
-
218
- /**
219
- * Load model from local files (Node.js)
220
- * @param {string} modelPath - Path to ONNX model
221
- * @param {string} tokenizerPath - Path to tokenizer.json
222
- * @returns {Promise<{modelBytes: Uint8Array, tokenizerJson: string}>}
223
- */
224
- async loadFromFiles(modelPath, tokenizerPath) {
225
- // Node.js environment
226
- if (typeof process !== 'undefined' && process.versions?.node) {
227
- const fs = await import('fs/promises');
228
- const [modelBytes, tokenizerJson] = await Promise.all([
229
- fs.readFile(modelPath),
230
- fs.readFile(tokenizerPath, 'utf8'),
231
- ]);
232
- return {
233
- modelBytes: new Uint8Array(modelBytes),
234
- tokenizerJson,
235
- };
236
- }
237
- throw new Error('loadFromFiles is only available in Node.js');
238
- }
239
-
240
- /**
241
- * Fetch with optional caching (uses Cache API in browsers)
242
- */
243
- async fetchWithCache(url, cacheKey, responseType) {
244
- // Try cache first (browser only)
245
- if (this.cache && typeof caches !== 'undefined' && cacheKey) {
246
- try {
247
- const cache = await caches.open(this.cacheStorage);
248
- const cached = await cache.match(cacheKey);
249
- if (cached) {
250
- console.log(` Cache hit: ${cacheKey}`);
251
- return responseType === 'arraybuffer'
252
- ? await cached.arrayBuffer()
253
- : await cached.text();
254
- }
255
- } catch (e) {
256
- // Cache API not available, continue with fetch
257
- }
258
- }
259
-
260
- // Fetch from network
261
- console.log(` Downloading: ${url}`);
262
- const response = await this.fetchWithProgress(url);
263
-
264
- if (!response.ok) {
265
- throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
266
- }
267
-
268
- // Clone for caching
269
- const responseClone = response.clone();
270
-
271
- // Cache the response (browser only)
272
- if (this.cache && typeof caches !== 'undefined' && cacheKey) {
273
- try {
274
- const cache = await caches.open(this.cacheStorage);
275
- await cache.put(cacheKey, responseClone);
276
- } catch (e) {
277
- // Cache write failed, continue
278
- }
279
- }
280
-
281
- return responseType === 'arraybuffer'
282
- ? await response.arrayBuffer()
283
- : await response.text();
284
- }
285
-
286
- /**
287
- * Fetch with progress reporting
288
- */
289
- async fetchWithProgress(url) {
290
- const response = await fetch(url);
291
-
292
- if (!this.onProgress || !response.body) {
293
- return response;
294
- }
295
-
296
- const contentLength = response.headers.get('content-length');
297
- if (!contentLength) {
298
- return response;
299
- }
300
-
301
- const total = parseInt(contentLength, 10);
302
- let loaded = 0;
303
-
304
- const reader = response.body.getReader();
305
- const chunks = [];
306
-
307
- while (true) {
308
- const { done, value } = await reader.read();
309
- if (done) break;
310
-
311
- chunks.push(value);
312
- loaded += value.length;
313
-
314
- this.onProgress({
315
- loaded,
316
- total,
317
- percent: Math.round((loaded / total) * 100),
318
- });
319
- }
320
-
321
- const body = new Uint8Array(loaded);
322
- let position = 0;
323
- for (const chunk of chunks) {
324
- body.set(chunk, position);
325
- position += chunk.length;
326
- }
327
-
328
- return new Response(body, {
329
- headers: response.headers,
330
- status: response.status,
331
- statusText: response.statusText,
332
- });
333
- }
334
-
335
- /**
336
- * Clear cached models
337
- */
338
- async clearCache() {
339
- if (typeof caches !== 'undefined') {
340
- await caches.delete(this.cacheStorage);
341
- console.log('Model cache cleared');
342
- }
343
- }
344
-
345
- /**
346
- * List available models
347
- */
348
- static listModels() {
349
- return Object.entries(MODELS).map(([key, config]) => ({
350
- id: key,
351
- ...config,
352
- }));
353
- }
354
- }
355
-
356
- /**
357
- * Quick helper to create an embedder with a pre-configured model
358
- *
359
- * @example
360
- * ```javascript
361
- * import { createEmbedder } from './loader.js';
362
- *
363
- * const embedder = await createEmbedder('all-MiniLM-L6-v2');
364
- * const embedding = embedder.embedOne("Hello world");
365
- * ```
366
- */
367
- export async function createEmbedder(modelName = DEFAULT_MODEL, wasmModule = null) {
368
- // Import WASM module if not provided
369
- if (!wasmModule) {
370
- wasmModule = await import('./pkg/ruvector_onnx_embeddings_wasm.js');
371
- await wasmModule.default();
372
- }
373
-
374
- const loader = new ModelLoader();
375
- const { modelBytes, tokenizerJson, config } = await loader.loadModel(modelName);
376
-
377
- const embedderConfig = new wasmModule.WasmEmbedderConfig()
378
- .setMaxLength(config.maxLength)
379
- .setNormalize(true)
380
- .setPooling(0); // Mean pooling
381
-
382
- const embedder = wasmModule.WasmEmbedder.withConfig(
383
- modelBytes,
384
- tokenizerJson,
385
- embedderConfig
386
- );
387
-
388
- return embedder;
389
- }
390
-
391
- /**
392
- * Quick helper for one-off embedding (loads model, embeds, returns)
393
- *
394
- * @example
395
- * ```javascript
396
- * import { embed } from './loader.js';
397
- *
398
- * const embedding = await embed("Hello world");
399
- * const embeddings = await embed(["Hello", "World"]);
400
- * ```
401
- */
402
- export async function embed(text, modelName = DEFAULT_MODEL) {
403
- const embedder = await createEmbedder(modelName);
404
-
405
- if (Array.isArray(text)) {
406
- return embedder.embedBatch(text);
407
- }
408
- return embedder.embedOne(text);
409
- }
410
-
411
- /**
412
- * Quick helper for similarity comparison
413
- *
414
- * @example
415
- * ```javascript
416
- * import { similarity } from './loader.js';
417
- *
418
- * const score = await similarity("I love dogs", "I adore puppies");
419
- * console.log(score); // ~0.85
420
- * ```
421
- */
422
- export async function similarity(text1, text2, modelName = DEFAULT_MODEL) {
423
- const embedder = await createEmbedder(modelName);
424
- return embedder.similarity(text1, text2);
425
- }
426
-
427
- export default {
428
- MODELS,
429
- DEFAULT_MODEL,
430
- ModelLoader,
431
- createEmbedder,
432
- embed,
433
- similarity,
434
- };
1
+ /**
2
+ * Model Loader for RuVector ONNX Embeddings WASM
3
+ *
4
+ * Provides easy loading of pre-trained models from HuggingFace Hub
5
+ */
6
+
7
+ /**
8
+ * Pre-configured models with their HuggingFace URLs
9
+ */
10
+ export const MODELS = {
11
+ // Sentence Transformers - Small & Fast
12
+ 'all-MiniLM-L6-v2': {
13
+ name: 'all-MiniLM-L6-v2',
14
+ dimension: 384,
15
+ maxLength: 256,
16
+ size: '23MB',
17
+ description: 'Fast, general-purpose embeddings',
18
+ model: 'https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx',
19
+ tokenizer: 'https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json',
20
+ },
21
+ 'all-MiniLM-L12-v2': {
22
+ name: 'all-MiniLM-L12-v2',
23
+ dimension: 384,
24
+ maxLength: 256,
25
+ size: '33MB',
26
+ description: 'Better quality, balanced speed',
27
+ model: 'https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2/resolve/main/onnx/model.onnx',
28
+ tokenizer: 'https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2/resolve/main/tokenizer.json',
29
+ },
30
+
31
+ // BGE Models - State of the art
32
+ 'bge-small-en-v1.5': {
33
+ name: 'bge-small-en-v1.5',
34
+ dimension: 384,
35
+ maxLength: 512,
36
+ size: '33MB',
37
+ description: 'State-of-the-art small model',
38
+ model: 'https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/onnx/model.onnx',
39
+ tokenizer: 'https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/tokenizer.json',
40
+ },
41
+ 'bge-base-en-v1.5': {
42
+ name: 'bge-base-en-v1.5',
43
+ dimension: 768,
44
+ maxLength: 512,
45
+ size: '110MB',
46
+ description: 'Best overall quality',
47
+ model: 'https://huggingface.co/BAAI/bge-base-en-v1.5/resolve/main/onnx/model.onnx',
48
+ tokenizer: 'https://huggingface.co/BAAI/bge-base-en-v1.5/resolve/main/tokenizer.json',
49
+ },
50
+
51
+ // E5 Models - Microsoft
52
+ 'e5-small-v2': {
53
+ name: 'e5-small-v2',
54
+ dimension: 384,
55
+ maxLength: 512,
56
+ size: '33MB',
57
+ description: 'Excellent for search & retrieval',
58
+ model: 'https://huggingface.co/intfloat/e5-small-v2/resolve/main/onnx/model.onnx',
59
+ tokenizer: 'https://huggingface.co/intfloat/e5-small-v2/resolve/main/tokenizer.json',
60
+ },
61
+
62
+ // GTE Models - Alibaba
63
+ 'gte-small': {
64
+ name: 'gte-small',
65
+ dimension: 384,
66
+ maxLength: 512,
67
+ size: '33MB',
68
+ description: 'Good multilingual support',
69
+ model: 'https://huggingface.co/thenlper/gte-small/resolve/main/onnx/model.onnx',
70
+ tokenizer: 'https://huggingface.co/thenlper/gte-small/resolve/main/tokenizer.json',
71
+ },
72
+ };
73
+
74
+ /**
75
+ * Default model for quick start
76
+ */
77
+ export const DEFAULT_MODEL = 'all-MiniLM-L6-v2';
78
+
79
+ /**
80
+ * In-memory memo of loaded models, keyed by model name. Deduplicates the
81
+ * (re-)download + decode when multiple embedder instances load the same model
82
+ * in one process. In Node there is no Cache API, so without this every
83
+ * ModelLoader.loadModel() re-fetches the model from HuggingFace (issue #523).
84
+ */
85
+ const _inMemoryModelCache = new Map();
86
+
87
+ /**
88
+ * Model loader with caching support
89
+ */
90
+ export class ModelLoader {
91
+ constructor(options = {}) {
92
+ this.cache = options.cache ?? true;
93
+ this.cacheStorage = options.cacheStorage ?? 'ruvector-models';
94
+ this.onProgress = options.onProgress ?? null;
95
+ }
96
+
97
+ /**
98
+ * Load a pre-configured model by name
99
+ * @param {string} modelName - Model name from MODELS
100
+ * @returns {Promise<{modelBytes: Uint8Array, tokenizerJson: string, config: object}>}
101
+ */
102
+ async loadModel(modelName = DEFAULT_MODEL) {
103
+ const modelConfig = MODELS[modelName];
104
+ if (!modelConfig) {
105
+ throw new Error(`Unknown model: ${modelName}. Available: ${Object.keys(MODELS).join(', ')}`);
106
+ }
107
+
108
+ // In-memory memo: a second load of the same model in this process reuses
109
+ // the already-downloaded bytes instead of re-fetching (issue #523).
110
+ if (this.cache && _inMemoryModelCache.has(modelName)) {
111
+ return _inMemoryModelCache.get(modelName);
112
+ }
113
+
114
+ // On-disk cache (Node only): models persist across processes so they are
115
+ // downloaded once, not every run. The browser has the Cache API instead
116
+ // (handled in fetchWithCache). See issue #523.
117
+ if (this.cache) {
118
+ const disk = await this._loadFromDisk(modelName);
119
+ if (disk) {
120
+ const cached = { ...disk, config: modelConfig };
121
+ _inMemoryModelCache.set(modelName, cached);
122
+ return cached;
123
+ }
124
+ }
125
+
126
+ console.log(`Loading model: ${modelConfig.name} (${modelConfig.size})`);
127
+
128
+ const [modelBytes, tokenizerJson] = await Promise.all([
129
+ this.fetchWithCache(modelConfig.model, `${modelName}-model.onnx`, 'arraybuffer'),
130
+ this.fetchWithCache(modelConfig.tokenizer, `${modelName}-tokenizer.json`, 'text'),
131
+ ]);
132
+
133
+ const result = {
134
+ modelBytes: new Uint8Array(modelBytes),
135
+ tokenizerJson,
136
+ config: modelConfig,
137
+ };
138
+
139
+ if (this.cache) {
140
+ _inMemoryModelCache.set(modelName, result);
141
+ await this._saveToDisk(modelName, result.modelBytes, tokenizerJson);
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Resolve the Node on-disk cache dir for a model (null in non-Node envs).
149
+ * Uses dynamic import so this module stays loadable in browsers/bundlers.
150
+ */
151
+ async _diskCacheDir(modelName) {
152
+ if (typeof process === 'undefined' || !process.versions?.node) return null;
153
+ const home = process.env.RUVECTOR_CACHE_DIR
154
+ || process.env.HOME || process.env.USERPROFILE || '/tmp';
155
+ const path = await import('node:path');
156
+ return path.join(home, '.ruvector', 'models', modelName);
157
+ }
158
+
159
+ /** Load model bytes + tokenizer from the Node disk cache, or null if absent. */
160
+ async _loadFromDisk(modelName) {
161
+ const dir = await this._diskCacheDir(modelName);
162
+ if (!dir) return null;
163
+ try {
164
+ const fs = await import('node:fs');
165
+ const path = await import('node:path');
166
+ const modelPath = path.join(dir, 'model.onnx');
167
+ const tokPath = path.join(dir, 'tokenizer.json');
168
+ if (!fs.existsSync(modelPath) || !fs.existsSync(tokPath)) return null;
169
+ const modelBytes = new Uint8Array(fs.readFileSync(modelPath));
170
+ const tokenizerJson = fs.readFileSync(tokPath, 'utf8');
171
+ if (modelBytes.length === 0 || tokenizerJson.length === 0) return null;
172
+ console.log(` Disk cache hit: ${modelName}`);
173
+ return { modelBytes, tokenizerJson };
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ /** Persist model bytes + tokenizer to the Node disk cache (best-effort). */
180
+ async _saveToDisk(modelName, modelBytes, tokenizerJson) {
181
+ const dir = await this._diskCacheDir(modelName);
182
+ if (!dir) return;
183
+ try {
184
+ const fs = await import('node:fs');
185
+ const path = await import('node:path');
186
+ fs.mkdirSync(dir, { recursive: true });
187
+ // Write to temp files then rename, so a crash mid-write can't leave a
188
+ // truncated cache entry that later reads would trust.
189
+ const mTmp = path.join(dir, 'model.onnx.tmp');
190
+ const tTmp = path.join(dir, 'tokenizer.json.tmp');
191
+ fs.writeFileSync(mTmp, Buffer.from(modelBytes));
192
+ fs.writeFileSync(tTmp, tokenizerJson);
193
+ fs.renameSync(mTmp, path.join(dir, 'model.onnx'));
194
+ fs.renameSync(tTmp, path.join(dir, 'tokenizer.json'));
195
+ } catch {
196
+ // Cache write is best-effort; embedding still works without it.
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Load model from custom URLs
202
+ * @param {string} modelUrl - URL to ONNX model
203
+ * @param {string} tokenizerUrl - URL to tokenizer.json
204
+ * @returns {Promise<{modelBytes: Uint8Array, tokenizerJson: string}>}
205
+ */
206
+ async loadFromUrls(modelUrl, tokenizerUrl) {
207
+ const [modelBytes, tokenizerJson] = await Promise.all([
208
+ this.fetchWithCache(modelUrl, null, 'arraybuffer'),
209
+ this.fetchWithCache(tokenizerUrl, null, 'text'),
210
+ ]);
211
+
212
+ return {
213
+ modelBytes: new Uint8Array(modelBytes),
214
+ tokenizerJson,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Load model from local files (Node.js)
220
+ * @param {string} modelPath - Path to ONNX model
221
+ * @param {string} tokenizerPath - Path to tokenizer.json
222
+ * @returns {Promise<{modelBytes: Uint8Array, tokenizerJson: string}>}
223
+ */
224
+ async loadFromFiles(modelPath, tokenizerPath) {
225
+ // Node.js environment
226
+ if (typeof process !== 'undefined' && process.versions?.node) {
227
+ const fs = await import('fs/promises');
228
+ const [modelBytes, tokenizerJson] = await Promise.all([
229
+ fs.readFile(modelPath),
230
+ fs.readFile(tokenizerPath, 'utf8'),
231
+ ]);
232
+ return {
233
+ modelBytes: new Uint8Array(modelBytes),
234
+ tokenizerJson,
235
+ };
236
+ }
237
+ throw new Error('loadFromFiles is only available in Node.js');
238
+ }
239
+
240
+ /**
241
+ * Fetch with optional caching (uses Cache API in browsers)
242
+ */
243
+ async fetchWithCache(url, cacheKey, responseType) {
244
+ // Try cache first (browser only)
245
+ if (this.cache && typeof caches !== 'undefined' && cacheKey) {
246
+ try {
247
+ const cache = await caches.open(this.cacheStorage);
248
+ const cached = await cache.match(cacheKey);
249
+ if (cached) {
250
+ console.log(` Cache hit: ${cacheKey}`);
251
+ return responseType === 'arraybuffer'
252
+ ? await cached.arrayBuffer()
253
+ : await cached.text();
254
+ }
255
+ } catch (e) {
256
+ // Cache API not available, continue with fetch
257
+ }
258
+ }
259
+
260
+ // Fetch from network
261
+ console.log(` Downloading: ${url}`);
262
+ const response = await this.fetchWithProgress(url);
263
+
264
+ if (!response.ok) {
265
+ throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
266
+ }
267
+
268
+ // Clone for caching
269
+ const responseClone = response.clone();
270
+
271
+ // Cache the response (browser only)
272
+ if (this.cache && typeof caches !== 'undefined' && cacheKey) {
273
+ try {
274
+ const cache = await caches.open(this.cacheStorage);
275
+ await cache.put(cacheKey, responseClone);
276
+ } catch (e) {
277
+ // Cache write failed, continue
278
+ }
279
+ }
280
+
281
+ return responseType === 'arraybuffer'
282
+ ? await response.arrayBuffer()
283
+ : await response.text();
284
+ }
285
+
286
+ /**
287
+ * Fetch with progress reporting
288
+ */
289
+ async fetchWithProgress(url) {
290
+ const response = await fetch(url);
291
+
292
+ if (!this.onProgress || !response.body) {
293
+ return response;
294
+ }
295
+
296
+ const contentLength = response.headers.get('content-length');
297
+ if (!contentLength) {
298
+ return response;
299
+ }
300
+
301
+ const total = parseInt(contentLength, 10);
302
+ let loaded = 0;
303
+
304
+ const reader = response.body.getReader();
305
+ const chunks = [];
306
+
307
+ while (true) {
308
+ const { done, value } = await reader.read();
309
+ if (done) break;
310
+
311
+ chunks.push(value);
312
+ loaded += value.length;
313
+
314
+ this.onProgress({
315
+ loaded,
316
+ total,
317
+ percent: Math.round((loaded / total) * 100),
318
+ });
319
+ }
320
+
321
+ const body = new Uint8Array(loaded);
322
+ let position = 0;
323
+ for (const chunk of chunks) {
324
+ body.set(chunk, position);
325
+ position += chunk.length;
326
+ }
327
+
328
+ return new Response(body, {
329
+ headers: response.headers,
330
+ status: response.status,
331
+ statusText: response.statusText,
332
+ });
333
+ }
334
+
335
+ /**
336
+ * Clear cached models
337
+ */
338
+ async clearCache() {
339
+ if (typeof caches !== 'undefined') {
340
+ await caches.delete(this.cacheStorage);
341
+ console.log('Model cache cleared');
342
+ }
343
+ }
344
+
345
+ /**
346
+ * List available models
347
+ */
348
+ static listModels() {
349
+ return Object.entries(MODELS).map(([key, config]) => ({
350
+ id: key,
351
+ ...config,
352
+ }));
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Quick helper to create an embedder with a pre-configured model
358
+ *
359
+ * @example
360
+ * ```javascript
361
+ * import { createEmbedder } from './loader.js';
362
+ *
363
+ * const embedder = await createEmbedder('all-MiniLM-L6-v2');
364
+ * const embedding = embedder.embedOne("Hello world");
365
+ * ```
366
+ */
367
+ export async function createEmbedder(modelName = DEFAULT_MODEL, wasmModule = null) {
368
+ // Import WASM module if not provided
369
+ if (!wasmModule) {
370
+ wasmModule = await import('./pkg/ruvector_onnx_embeddings_wasm.js');
371
+ await wasmModule.default();
372
+ }
373
+
374
+ const loader = new ModelLoader();
375
+ const { modelBytes, tokenizerJson, config } = await loader.loadModel(modelName);
376
+
377
+ const embedderConfig = new wasmModule.WasmEmbedderConfig()
378
+ .setMaxLength(config.maxLength)
379
+ .setNormalize(true)
380
+ .setPooling(0); // Mean pooling
381
+
382
+ const embedder = wasmModule.WasmEmbedder.withConfig(
383
+ modelBytes,
384
+ tokenizerJson,
385
+ embedderConfig
386
+ );
387
+
388
+ return embedder;
389
+ }
390
+
391
+ /**
392
+ * Quick helper for one-off embedding (loads model, embeds, returns)
393
+ *
394
+ * @example
395
+ * ```javascript
396
+ * import { embed } from './loader.js';
397
+ *
398
+ * const embedding = await embed("Hello world");
399
+ * const embeddings = await embed(["Hello", "World"]);
400
+ * ```
401
+ */
402
+ export async function embed(text, modelName = DEFAULT_MODEL) {
403
+ const embedder = await createEmbedder(modelName);
404
+
405
+ if (Array.isArray(text)) {
406
+ return embedder.embedBatch(text);
407
+ }
408
+ return embedder.embedOne(text);
409
+ }
410
+
411
+ /**
412
+ * Quick helper for similarity comparison
413
+ *
414
+ * @example
415
+ * ```javascript
416
+ * import { similarity } from './loader.js';
417
+ *
418
+ * const score = await similarity("I love dogs", "I adore puppies");
419
+ * console.log(score); // ~0.85
420
+ * ```
421
+ */
422
+ export async function similarity(text1, text2, modelName = DEFAULT_MODEL) {
423
+ const embedder = await createEmbedder(modelName);
424
+ return embedder.similarity(text1, text2);
425
+ }
426
+
427
+ export default {
428
+ MODELS,
429
+ DEFAULT_MODEL,
430
+ ModelLoader,
431
+ createEmbedder,
432
+ embed,
433
+ similarity,
434
+ };