@wiscale/velesdb-sdk 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,647 @@
1
+ // src/types.ts
2
+ var VelesDBError = class extends Error {
3
+ constructor(message, code, cause) {
4
+ super(message);
5
+ this.code = code;
6
+ this.cause = cause;
7
+ this.name = "VelesDBError";
8
+ }
9
+ };
10
+ var ConnectionError = class extends VelesDBError {
11
+ constructor(message, cause) {
12
+ super(message, "CONNECTION_ERROR", cause);
13
+ this.name = "ConnectionError";
14
+ }
15
+ };
16
+ var ValidationError = class extends VelesDBError {
17
+ constructor(message) {
18
+ super(message, "VALIDATION_ERROR");
19
+ this.name = "ValidationError";
20
+ }
21
+ };
22
+ var NotFoundError = class extends VelesDBError {
23
+ constructor(resource) {
24
+ super(`${resource} not found`, "NOT_FOUND");
25
+ this.name = "NotFoundError";
26
+ }
27
+ };
28
+
29
+ // src/backends/wasm.ts
30
+ var WasmBackend = class {
31
+ constructor() {
32
+ this.wasmModule = null;
33
+ this.collections = /* @__PURE__ */ new Map();
34
+ this._initialized = false;
35
+ }
36
+ async init() {
37
+ if (this._initialized) {
38
+ return;
39
+ }
40
+ try {
41
+ this.wasmModule = await import("velesdb-wasm");
42
+ await this.wasmModule.default();
43
+ this._initialized = true;
44
+ } catch (error) {
45
+ throw new ConnectionError(
46
+ "Failed to initialize WASM module",
47
+ error instanceof Error ? error : void 0
48
+ );
49
+ }
50
+ }
51
+ isInitialized() {
52
+ return this._initialized;
53
+ }
54
+ ensureInitialized() {
55
+ if (!this._initialized || !this.wasmModule) {
56
+ throw new ConnectionError("WASM backend not initialized");
57
+ }
58
+ }
59
+ async createCollection(name, config) {
60
+ this.ensureInitialized();
61
+ if (this.collections.has(name)) {
62
+ throw new VelesDBError(`Collection '${name}' already exists`, "COLLECTION_EXISTS");
63
+ }
64
+ const metric = config.metric ?? "cosine";
65
+ const store = new this.wasmModule.VectorStore(config.dimension, metric);
66
+ this.collections.set(name, {
67
+ config: { ...config, metric },
68
+ store,
69
+ payloads: /* @__PURE__ */ new Map(),
70
+ createdAt: /* @__PURE__ */ new Date()
71
+ });
72
+ }
73
+ async deleteCollection(name) {
74
+ this.ensureInitialized();
75
+ const collection = this.collections.get(name);
76
+ if (!collection) {
77
+ throw new NotFoundError(`Collection '${name}'`);
78
+ }
79
+ collection.store.free();
80
+ this.collections.delete(name);
81
+ }
82
+ async getCollection(name) {
83
+ this.ensureInitialized();
84
+ const collection = this.collections.get(name);
85
+ if (!collection) {
86
+ return null;
87
+ }
88
+ return {
89
+ name,
90
+ dimension: collection.config.dimension,
91
+ metric: collection.config.metric ?? "cosine",
92
+ count: collection.store.len,
93
+ createdAt: collection.createdAt
94
+ };
95
+ }
96
+ async listCollections() {
97
+ this.ensureInitialized();
98
+ const result = [];
99
+ for (const [name, data] of this.collections) {
100
+ result.push({
101
+ name,
102
+ dimension: data.config.dimension,
103
+ metric: data.config.metric ?? "cosine",
104
+ count: data.store.len,
105
+ createdAt: data.createdAt
106
+ });
107
+ }
108
+ return result;
109
+ }
110
+ async insert(collectionName, doc) {
111
+ this.ensureInitialized();
112
+ const collection = this.collections.get(collectionName);
113
+ if (!collection) {
114
+ throw new NotFoundError(`Collection '${collectionName}'`);
115
+ }
116
+ const id = this.toNumericId(doc.id);
117
+ const vector = doc.vector instanceof Float32Array ? doc.vector : new Float32Array(doc.vector);
118
+ if (vector.length !== collection.config.dimension) {
119
+ throw new VelesDBError(
120
+ `Vector dimension mismatch: expected ${collection.config.dimension}, got ${vector.length}`,
121
+ "DIMENSION_MISMATCH"
122
+ );
123
+ }
124
+ collection.store.insert(BigInt(id), vector);
125
+ if (doc.payload) {
126
+ collection.payloads.set(String(doc.id), doc.payload);
127
+ }
128
+ }
129
+ async insertBatch(collectionName, docs) {
130
+ this.ensureInitialized();
131
+ const collection = this.collections.get(collectionName);
132
+ if (!collection) {
133
+ throw new NotFoundError(`Collection '${collectionName}'`);
134
+ }
135
+ for (const doc of docs) {
136
+ const vectorLen = Array.isArray(doc.vector) ? doc.vector.length : doc.vector.length;
137
+ if (vectorLen !== collection.config.dimension) {
138
+ throw new VelesDBError(
139
+ `Vector dimension mismatch for doc ${doc.id}: expected ${collection.config.dimension}, got ${vectorLen}`,
140
+ "DIMENSION_MISMATCH"
141
+ );
142
+ }
143
+ }
144
+ collection.store.reserve(docs.length);
145
+ const batch = docs.map((doc) => [
146
+ BigInt(this.toNumericId(doc.id)),
147
+ Array.isArray(doc.vector) ? doc.vector : Array.from(doc.vector)
148
+ ]);
149
+ collection.store.insert_batch(batch);
150
+ for (const doc of docs) {
151
+ if (doc.payload) {
152
+ collection.payloads.set(String(doc.id), doc.payload);
153
+ }
154
+ }
155
+ }
156
+ async search(collectionName, query, options) {
157
+ this.ensureInitialized();
158
+ const collection = this.collections.get(collectionName);
159
+ if (!collection) {
160
+ throw new NotFoundError(`Collection '${collectionName}'`);
161
+ }
162
+ const queryVector = query instanceof Float32Array ? query : new Float32Array(query);
163
+ if (queryVector.length !== collection.config.dimension) {
164
+ throw new VelesDBError(
165
+ `Query dimension mismatch: expected ${collection.config.dimension}, got ${queryVector.length}`,
166
+ "DIMENSION_MISMATCH"
167
+ );
168
+ }
169
+ const k = options?.k ?? 10;
170
+ const rawResults = collection.store.search(queryVector, k);
171
+ return rawResults.map(([id, score]) => {
172
+ const stringId = String(id);
173
+ const result = {
174
+ id: stringId,
175
+ score
176
+ };
177
+ const payload = collection.payloads.get(stringId);
178
+ if (payload) {
179
+ result.payload = payload;
180
+ }
181
+ return result;
182
+ });
183
+ }
184
+ async delete(collectionName, id) {
185
+ this.ensureInitialized();
186
+ const collection = this.collections.get(collectionName);
187
+ if (!collection) {
188
+ throw new NotFoundError(`Collection '${collectionName}'`);
189
+ }
190
+ const numericId = this.toNumericId(id);
191
+ const removed = collection.store.remove(BigInt(numericId));
192
+ if (removed) {
193
+ collection.payloads.delete(String(id));
194
+ }
195
+ return removed;
196
+ }
197
+ async get(collectionName, id) {
198
+ this.ensureInitialized();
199
+ const collection = this.collections.get(collectionName);
200
+ if (!collection) {
201
+ throw new NotFoundError(`Collection '${collectionName}'`);
202
+ }
203
+ const payload = collection.payloads.get(String(id));
204
+ if (!payload) {
205
+ return null;
206
+ }
207
+ return {
208
+ id,
209
+ vector: [],
210
+ // Not available in current WASM impl
211
+ payload
212
+ };
213
+ }
214
+ async close() {
215
+ for (const [, data] of this.collections) {
216
+ data.store.free();
217
+ }
218
+ this.collections.clear();
219
+ this._initialized = false;
220
+ }
221
+ toNumericId(id) {
222
+ if (typeof id === "number") {
223
+ return id;
224
+ }
225
+ const parsed = parseInt(id, 10);
226
+ if (!isNaN(parsed)) {
227
+ return parsed;
228
+ }
229
+ let hash = 0;
230
+ for (let i = 0; i < id.length; i++) {
231
+ const char = id.charCodeAt(i);
232
+ hash = (hash << 5) - hash + char;
233
+ hash = hash & hash;
234
+ }
235
+ return Math.abs(hash);
236
+ }
237
+ };
238
+
239
+ // src/backends/rest.ts
240
+ var RestBackend = class {
241
+ constructor(url, apiKey, timeout = 3e4) {
242
+ this._initialized = false;
243
+ this.baseUrl = url.replace(/\/$/, "");
244
+ this.apiKey = apiKey;
245
+ this.timeout = timeout;
246
+ }
247
+ async init() {
248
+ if (this._initialized) {
249
+ return;
250
+ }
251
+ try {
252
+ const response = await this.request("GET", "/health");
253
+ if (response.error) {
254
+ throw new Error(response.error.message);
255
+ }
256
+ this._initialized = true;
257
+ } catch (error) {
258
+ throw new ConnectionError(
259
+ `Failed to connect to VelesDB server at ${this.baseUrl}`,
260
+ error instanceof Error ? error : void 0
261
+ );
262
+ }
263
+ }
264
+ isInitialized() {
265
+ return this._initialized;
266
+ }
267
+ ensureInitialized() {
268
+ if (!this._initialized) {
269
+ throw new ConnectionError("REST backend not initialized");
270
+ }
271
+ }
272
+ async request(method, path, body) {
273
+ const url = `${this.baseUrl}${path}`;
274
+ const headers = {
275
+ "Content-Type": "application/json"
276
+ };
277
+ if (this.apiKey) {
278
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
279
+ }
280
+ const controller = new AbortController();
281
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
282
+ try {
283
+ const response = await fetch(url, {
284
+ method,
285
+ headers,
286
+ body: body ? JSON.stringify(body) : void 0,
287
+ signal: controller.signal
288
+ });
289
+ clearTimeout(timeoutId);
290
+ const data = await response.json();
291
+ if (!response.ok) {
292
+ return {
293
+ error: {
294
+ code: data.code ?? "UNKNOWN_ERROR",
295
+ message: data.message ?? `HTTP ${response.status}`
296
+ }
297
+ };
298
+ }
299
+ return { data };
300
+ } catch (error) {
301
+ clearTimeout(timeoutId);
302
+ if (error instanceof Error && error.name === "AbortError") {
303
+ throw new ConnectionError("Request timeout");
304
+ }
305
+ throw new ConnectionError(
306
+ `Request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
307
+ error instanceof Error ? error : void 0
308
+ );
309
+ }
310
+ }
311
+ async createCollection(name, config) {
312
+ this.ensureInitialized();
313
+ const response = await this.request("POST", "/collections", {
314
+ name,
315
+ dimension: config.dimension,
316
+ metric: config.metric ?? "cosine",
317
+ description: config.description
318
+ });
319
+ if (response.error) {
320
+ throw new VelesDBError(response.error.message, response.error.code);
321
+ }
322
+ }
323
+ async deleteCollection(name) {
324
+ this.ensureInitialized();
325
+ const response = await this.request("DELETE", `/collections/${encodeURIComponent(name)}`);
326
+ if (response.error) {
327
+ if (response.error.code === "NOT_FOUND") {
328
+ throw new NotFoundError(`Collection '${name}'`);
329
+ }
330
+ throw new VelesDBError(response.error.message, response.error.code);
331
+ }
332
+ }
333
+ async getCollection(name) {
334
+ this.ensureInitialized();
335
+ const response = await this.request(
336
+ "GET",
337
+ `/collections/${encodeURIComponent(name)}`
338
+ );
339
+ if (response.error) {
340
+ if (response.error.code === "NOT_FOUND") {
341
+ return null;
342
+ }
343
+ throw new VelesDBError(response.error.message, response.error.code);
344
+ }
345
+ return response.data ?? null;
346
+ }
347
+ async listCollections() {
348
+ this.ensureInitialized();
349
+ const response = await this.request("GET", "/collections");
350
+ if (response.error) {
351
+ throw new VelesDBError(response.error.message, response.error.code);
352
+ }
353
+ return response.data ?? [];
354
+ }
355
+ async insert(collection, doc) {
356
+ this.ensureInitialized();
357
+ const vector = doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector;
358
+ const response = await this.request(
359
+ "POST",
360
+ `/collections/${encodeURIComponent(collection)}/vectors`,
361
+ {
362
+ id: doc.id,
363
+ vector,
364
+ payload: doc.payload
365
+ }
366
+ );
367
+ if (response.error) {
368
+ if (response.error.code === "NOT_FOUND") {
369
+ throw new NotFoundError(`Collection '${collection}'`);
370
+ }
371
+ throw new VelesDBError(response.error.message, response.error.code);
372
+ }
373
+ }
374
+ async insertBatch(collection, docs) {
375
+ this.ensureInitialized();
376
+ const vectors = docs.map((doc) => ({
377
+ id: doc.id,
378
+ vector: doc.vector instanceof Float32Array ? Array.from(doc.vector) : doc.vector,
379
+ payload: doc.payload
380
+ }));
381
+ const response = await this.request(
382
+ "POST",
383
+ `/collections/${encodeURIComponent(collection)}/vectors/batch`,
384
+ { vectors }
385
+ );
386
+ if (response.error) {
387
+ if (response.error.code === "NOT_FOUND") {
388
+ throw new NotFoundError(`Collection '${collection}'`);
389
+ }
390
+ throw new VelesDBError(response.error.message, response.error.code);
391
+ }
392
+ }
393
+ async search(collection, query, options) {
394
+ this.ensureInitialized();
395
+ const queryVector = query instanceof Float32Array ? Array.from(query) : query;
396
+ const response = await this.request(
397
+ "POST",
398
+ `/collections/${encodeURIComponent(collection)}/search`,
399
+ {
400
+ vector: queryVector,
401
+ k: options?.k ?? 10,
402
+ filter: options?.filter,
403
+ include_vectors: options?.includeVectors ?? false
404
+ }
405
+ );
406
+ if (response.error) {
407
+ if (response.error.code === "NOT_FOUND") {
408
+ throw new NotFoundError(`Collection '${collection}'`);
409
+ }
410
+ throw new VelesDBError(response.error.message, response.error.code);
411
+ }
412
+ return response.data ?? [];
413
+ }
414
+ async delete(collection, id) {
415
+ this.ensureInitialized();
416
+ const response = await this.request(
417
+ "DELETE",
418
+ `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
419
+ );
420
+ if (response.error) {
421
+ if (response.error.code === "NOT_FOUND") {
422
+ return false;
423
+ }
424
+ throw new VelesDBError(response.error.message, response.error.code);
425
+ }
426
+ return response.data?.deleted ?? false;
427
+ }
428
+ async get(collection, id) {
429
+ this.ensureInitialized();
430
+ const response = await this.request(
431
+ "GET",
432
+ `/collections/${encodeURIComponent(collection)}/vectors/${encodeURIComponent(String(id))}`
433
+ );
434
+ if (response.error) {
435
+ if (response.error.code === "NOT_FOUND") {
436
+ return null;
437
+ }
438
+ throw new VelesDBError(response.error.message, response.error.code);
439
+ }
440
+ return response.data ?? null;
441
+ }
442
+ async close() {
443
+ this._initialized = false;
444
+ }
445
+ };
446
+
447
+ // src/client.ts
448
+ var VelesDB = class {
449
+ /**
450
+ * Create a new VelesDB client
451
+ *
452
+ * @param config - Client configuration
453
+ * @throws {ValidationError} If configuration is invalid
454
+ */
455
+ constructor(config) {
456
+ this.initialized = false;
457
+ this.validateConfig(config);
458
+ this.config = config;
459
+ this.backend = this.createBackend(config);
460
+ }
461
+ validateConfig(config) {
462
+ if (!config.backend) {
463
+ throw new ValidationError("Backend type is required");
464
+ }
465
+ if (config.backend !== "wasm" && config.backend !== "rest") {
466
+ throw new ValidationError(`Invalid backend type: ${config.backend}. Use 'wasm' or 'rest'`);
467
+ }
468
+ if (config.backend === "rest" && !config.url) {
469
+ throw new ValidationError("URL is required for REST backend");
470
+ }
471
+ }
472
+ createBackend(config) {
473
+ switch (config.backend) {
474
+ case "wasm":
475
+ return new WasmBackend();
476
+ case "rest":
477
+ return new RestBackend(config.url, config.apiKey, config.timeout);
478
+ default:
479
+ throw new ValidationError(`Unknown backend: ${config.backend}`);
480
+ }
481
+ }
482
+ /**
483
+ * Initialize the client
484
+ * Must be called before any other operations
485
+ */
486
+ async init() {
487
+ if (this.initialized) {
488
+ return;
489
+ }
490
+ await this.backend.init();
491
+ this.initialized = true;
492
+ }
493
+ /**
494
+ * Check if client is initialized
495
+ */
496
+ isInitialized() {
497
+ return this.initialized;
498
+ }
499
+ ensureInitialized() {
500
+ if (!this.initialized) {
501
+ throw new ValidationError("Client not initialized. Call init() first.");
502
+ }
503
+ }
504
+ /**
505
+ * Create a new collection
506
+ *
507
+ * @param name - Collection name
508
+ * @param config - Collection configuration
509
+ */
510
+ async createCollection(name, config) {
511
+ this.ensureInitialized();
512
+ if (!name || typeof name !== "string") {
513
+ throw new ValidationError("Collection name must be a non-empty string");
514
+ }
515
+ if (!config.dimension || config.dimension <= 0) {
516
+ throw new ValidationError("Dimension must be a positive integer");
517
+ }
518
+ await this.backend.createCollection(name, config);
519
+ }
520
+ /**
521
+ * Delete a collection
522
+ *
523
+ * @param name - Collection name
524
+ */
525
+ async deleteCollection(name) {
526
+ this.ensureInitialized();
527
+ await this.backend.deleteCollection(name);
528
+ }
529
+ /**
530
+ * Get collection information
531
+ *
532
+ * @param name - Collection name
533
+ * @returns Collection info or null if not found
534
+ */
535
+ async getCollection(name) {
536
+ this.ensureInitialized();
537
+ return this.backend.getCollection(name);
538
+ }
539
+ /**
540
+ * List all collections
541
+ *
542
+ * @returns Array of collections
543
+ */
544
+ async listCollections() {
545
+ this.ensureInitialized();
546
+ return this.backend.listCollections();
547
+ }
548
+ /**
549
+ * Insert a vector document
550
+ *
551
+ * @param collection - Collection name
552
+ * @param doc - Document to insert
553
+ */
554
+ async insert(collection, doc) {
555
+ this.ensureInitialized();
556
+ this.validateDocument(doc);
557
+ await this.backend.insert(collection, doc);
558
+ }
559
+ /**
560
+ * Insert multiple vector documents
561
+ *
562
+ * @param collection - Collection name
563
+ * @param docs - Documents to insert
564
+ */
565
+ async insertBatch(collection, docs) {
566
+ this.ensureInitialized();
567
+ if (!Array.isArray(docs)) {
568
+ throw new ValidationError("Documents must be an array");
569
+ }
570
+ for (const doc of docs) {
571
+ this.validateDocument(doc);
572
+ }
573
+ await this.backend.insertBatch(collection, docs);
574
+ }
575
+ validateDocument(doc) {
576
+ if (doc.id === void 0 || doc.id === null) {
577
+ throw new ValidationError("Document ID is required");
578
+ }
579
+ if (!doc.vector) {
580
+ throw new ValidationError("Document vector is required");
581
+ }
582
+ if (!Array.isArray(doc.vector) && !(doc.vector instanceof Float32Array)) {
583
+ throw new ValidationError("Vector must be an array or Float32Array");
584
+ }
585
+ }
586
+ /**
587
+ * Search for similar vectors
588
+ *
589
+ * @param collection - Collection name
590
+ * @param query - Query vector
591
+ * @param options - Search options
592
+ * @returns Search results sorted by relevance
593
+ */
594
+ async search(collection, query, options) {
595
+ this.ensureInitialized();
596
+ if (!query || !Array.isArray(query) && !(query instanceof Float32Array)) {
597
+ throw new ValidationError("Query must be an array or Float32Array");
598
+ }
599
+ return this.backend.search(collection, query, options);
600
+ }
601
+ /**
602
+ * Delete a vector by ID
603
+ *
604
+ * @param collection - Collection name
605
+ * @param id - Document ID
606
+ * @returns true if deleted, false if not found
607
+ */
608
+ async delete(collection, id) {
609
+ this.ensureInitialized();
610
+ return this.backend.delete(collection, id);
611
+ }
612
+ /**
613
+ * Get a vector by ID
614
+ *
615
+ * @param collection - Collection name
616
+ * @param id - Document ID
617
+ * @returns Document or null if not found
618
+ */
619
+ async get(collection, id) {
620
+ this.ensureInitialized();
621
+ return this.backend.get(collection, id);
622
+ }
623
+ /**
624
+ * Close the client and release resources
625
+ */
626
+ async close() {
627
+ if (this.initialized) {
628
+ await this.backend.close();
629
+ this.initialized = false;
630
+ }
631
+ }
632
+ /**
633
+ * Get the current backend type
634
+ */
635
+ get backendType() {
636
+ return this.config.backend;
637
+ }
638
+ };
639
+ export {
640
+ ConnectionError,
641
+ NotFoundError,
642
+ RestBackend,
643
+ ValidationError,
644
+ VelesDB,
645
+ VelesDBError,
646
+ WasmBackend
647
+ };
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@wiscale/velesdb-sdk",
3
+ "version": "0.5.0",
4
+ "description": "Official TypeScript SDK for VelesDB - Vector Search in Microseconds",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "test:coverage": "vitest run --coverage",
25
+ "lint": "eslint src --ext .ts",
26
+ "lint:fix": "eslint src --ext .ts --fix",
27
+ "format": "prettier --write src",
28
+ "typecheck": "tsc --noEmit",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "keywords": [
32
+ "velesdb",
33
+ "vector",
34
+ "database",
35
+ "similarity-search",
36
+ "embeddings",
37
+ "ai",
38
+ "machine-learning",
39
+ "wasm",
40
+ "typescript"
41
+ ],
42
+ "author": "Wiscale France",
43
+ "license": "Elastic-2.0",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "https://github.com/cyberlife-coder/VelesDB.git",
47
+ "directory": "sdks/typescript"
48
+ },
49
+ "homepage": "https://github.com/cyberlife-coder/VelesDB#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/cyberlife-coder/VelesDB/issues"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.10.0",
55
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
56
+ "@typescript-eslint/parser": "^6.0.0",
57
+ "@vitest/coverage-v8": "^4.0.16",
58
+ "eslint": "^8.0.0",
59
+ "eslint-config-prettier": "^9.0.0",
60
+ "prettier": "^3.0.0",
61
+ "tsup": "^8.0.0",
62
+ "typescript": "^5.3.0",
63
+ "vitest": "^4.0.16"
64
+ },
65
+ "dependencies": {
66
+ "velesdb-wasm": "^0.5.0"
67
+ },
68
+ "engines": {
69
+ "node": ">=18.0.0"
70
+ }
71
+ }