@qvac/llm-llamacpp 0.66.0 → 0.67.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/README.md +27 -3
- package/addon.d.ts +1 -0
- package/index.d.ts +55 -7
- package/index.js +59 -5
- package/package.json +1 -1
- package/prebuilds/android-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/darwin-arm64/qvac__llm-llamacpp.bare.exports +54 -29
- package/prebuilds/darwin-x64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/darwin-x64/qvac__llm-llamacpp.bare.exports +80 -29
- package/prebuilds/ios-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-arm64/qvac__llm-llamacpp.bare.exports +54 -29
- package/prebuilds/ios-arm64-simulator/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-arm64-simulator/qvac__llm-llamacpp.bare.exports +54 -29
- package/prebuilds/ios-x64-simulator/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/ios-x64-simulator/qvac__llm-llamacpp.bare.exports +80 -29
- package/prebuilds/linux-arm64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/linux-x64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/win32-x64/qvac__llm-llamacpp.bare +0 -0
- package/prebuilds/win32-x64/qvac__llm-llamacpp.bare.exports +0 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ This native C++ addon, built using the `Bare` Runtime, simplifies running Large
|
|
|
15
15
|
- [5. Load Model](#5-load-model)
|
|
16
16
|
- [6. Run Inference](#6-run-inference)
|
|
17
17
|
- [Tool calling](#tool-calling)
|
|
18
|
+
- [Ordered text and media](#ordered-text-and-media)
|
|
18
19
|
- [Batch inference](#batch-inference)
|
|
19
20
|
- [7. Release Resources](#7-release-resources)
|
|
20
21
|
- [API behavior by state](#api-behavior-by-state)
|
|
@@ -206,7 +207,8 @@ const config = {
|
|
|
206
207
|
| cache_checkpoints_max_bytes | integer (bytes) | 0 (unlimited) | Byte budget for those checkpoints, enforced before the count. The load fails with `InvalidArgument` when it cannot hold `cache_checkpoints` checkpoints of the largest size the context allows (on a sliding-window model, a full window). Also accepted as `cache-checkpoints-max-bytes` |
|
|
207
208
|
| cache_checkpoint_storage | `"memory"` or `"disk"` | `"memory"` | Where checkpoints and the per-request rollback snapshot live. `"memory"` keeps them in host RAM so a cached chat never touches the disk; `"disk"` writes them to a private directory inside `cache_checkpoint_dir`, which it requires. Also accepted as `cache-checkpoint-storage` |
|
|
208
209
|
| cache_checkpoint_dir | directory path | none | **Required with `cache_checkpoint_storage: "disk"`**, and refused without it: where checkpoint files go, inside a private directory the addon creates in it (mode 0700; on Windows, the directory itself). On mobile, pass the app's cache directory. The load fails with `InvalidArgument` when it is missing, empty, or unusable. Also accepted as `cache-checkpoint-dir` |
|
|
209
|
-
| cache_ram_mib | 0 – 1048576 (MiB) | 0 (off) | Host-RAM budget for conversations that are not running. A `cacheKey` switch or a batch slot eviction moves the conversation there instead of writing its file, and the next request on that key restores it. A write-back cache: unsaved turns reach the file on `saveCache()`, when the budget pushes the entry out, or at unload; ephemeral entries are dropped instead. Also accepted as `cache-ram-mib` ([details](./docs/cache-api.md#keep-switched-away-conversations-in-ram)) |
|
|
210
|
+
| cache_ram_mib | 0 – 1048576 (MiB) | 0 (off) | Host-RAM budget for conversations that are not running. A `cacheKey` switch or a batch slot eviction moves the conversation there instead of writing its file, and the next request on that key restores it; unless `cache_share_across_keys` is `"false"`, a request on another key that shares its prompt prefix starts from a copy of it. A write-back cache: unsaved turns reach the file on `saveCache()`, when the budget pushes the entry out, or at unload; ephemeral entries are dropped instead. Also accepted as `cache-ram-mib` ([details](./docs/cache-api.md#keep-switched-away-conversations-in-ram)) |
|
|
211
|
+
| cache_share_across_keys | `"true"`/`"on"`/`"1"`, `"false"`/`"off"`/`"0"` | `"true"` | Whether a request on a decoder-only model may start from state another conversation left (keyless memory, another key's state, a `cacheKey` file the model used). `"false"` keeps every request to its own key's state, for callers who must not learn through `cacheTokens` or timing whether another conversation shares their prompt. Also accepted as `cache-share-across-keys` ([details](./docs/cache-api.md#keep-conversations-apart)) |
|
|
210
212
|
| flash-attn | `"on"`/`"enabled"`/`"true"`/`"1"`, `"off"`/`"disabled"`/`"false"`/`"0"`, or `"auto"` | `"on"`, except when finetuning or on a BitNet model, where it is forced off | Flash attention. The four truthy and four falsey spellings are equivalent; `"auto"` is a third state that defers to qvac-fabric's runtime capability probe. Lower-case only — matching is case-sensitive and any other value is rejected. Affects the KV-cache auto-default — see below. Also accepted as `flash_attn`; supplying both spellings is an error |
|
|
211
213
|
| cache-type-k | `f16`, `f32`, `bf16`, `q8_0`, `q4_0`, … | auto (see below) | KV-cache **key** quantization type. Unset = auto-default (see KV-cache type below) |
|
|
212
214
|
| cache-type-v | `f16`, `f32`, `bf16`, `q8_0`, `q4_0`, … | auto (see below) | KV-cache **value** quantization type. Quantizing V requires `flash-attn` on |
|
|
@@ -345,6 +347,28 @@ const response = await model.run([
|
|
|
345
347
|
|
|
346
348
|
The tool grammar holds the model to the order of `parameters`. Inside an object it accepts the required properties first, in declared order, then the optional ones, also in declared order. When the model writes a property out of that order, the call isn't rejected with an error: the model drops a field, writes a broken value or never finishes the call. Declare `required` properties before optional ones at every level, and declare each group in the order the model is likely to write it. This applies to nested objects, and to every argument in formats that write the arguments as one JSON object (Qwen3). Qwen3.5's top-level `<parameter=...>` arguments are less strict: up to six required arguments in any order, then the optional ones in any order. An optional argument before a required one is still rejected there.
|
|
347
349
|
|
|
350
|
+
#### Ordered text and media
|
|
351
|
+
|
|
352
|
+
A user message's `content` is either a string or an ordered array of parts. Use the array when the position of an image inside the text matters — multiple-choice options that are pictures, illustrated forms, steps — so the model sees the text and every image in one turn, in order:
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
const response = await model.run([{
|
|
356
|
+
role: 'user',
|
|
357
|
+
content: [
|
|
358
|
+
{ type: 'text', text: 'Which image shows the same place as ' },
|
|
359
|
+
{ type: 'media', content: '/images/place.jpg' },
|
|
360
|
+
{ type: 'text', text: '?\nA. ' },
|
|
361
|
+
{ type: 'media', content: imageABytes }, // raw bytes work too
|
|
362
|
+
{ type: 'text', text: '\nB. ' },
|
|
363
|
+
{ type: 'media', content: '/images/option-b.jpg' }
|
|
364
|
+
]
|
|
365
|
+
}])
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Each media part becomes an `<__media__>` marker at its own position in the rendered turn — the same prompt llama-server builds from an OpenAI content-parts request. A part's `content` is an absolute path or a `Uint8Array`; media parts are only valid on user messages, and an unknown part type is rejected with `InvalidInputFormat`.
|
|
369
|
+
|
|
370
|
+
The alternative — one `{ role: 'user', type: 'media' }` message per part — keeps the order but renders each text part as its own user turn, so text that refers to "this image" ends up in a different turn from the image.
|
|
371
|
+
|
|
348
372
|
#### Batch inference
|
|
349
373
|
|
|
350
374
|
Load the model with `parallel >= 2`, then pass an array of prompts. Each chunk arrives tagged with the id of the sequence that produced it; `await()` returns results in input order.
|
|
@@ -443,7 +467,7 @@ Admission is controlled by `rejectWhenBusy` (instance-level `opts.rejectWhenBusy
|
|
|
443
467
|
|
|
444
468
|
#### Prefill (cache warming) with `parallel >= 2`
|
|
445
469
|
|
|
446
|
-
A prefill-only run (`runOptions.prefill: true`) is admitted on a parallel model only when its product survives the slot teardown, i.e. it has a `cacheKey`: the warmed conversation stays in its slot for the next request on that key. A keyless prefill warms
|
|
470
|
+
A prefill-only run (`runOptions.prefill: true`) is admitted on a parallel model only when its product survives the slot teardown, i.e. it has a `cacheKey`: the warmed conversation stays in its slot for the next request on that key. A keyless prefill warms state no later request can count on (at most parked anonymously, first to be evicted), so it is rejected with `InvalidArgument`; run it on a `parallel: 1` model instead. The same rule applies per item in batch runs. See [cache-api.md](./docs/cache-api.md).
|
|
447
471
|
|
|
448
472
|
#### Cancelling a batch
|
|
449
473
|
|
|
@@ -735,7 +759,7 @@ npm run test:cpp:models:ci # exactly what .github/workflows/cpp-tests-llm.yml
|
|
|
735
759
|
# downloads; matches what CI exercises
|
|
736
760
|
```
|
|
737
761
|
|
|
738
|
-
First-run downloads pull several GB from Hugging Face. Every fixture is SHA256-verified against a digest pinned in `scripts/download-unit-test-models.js`; mismatched or partial files are re-downloaded automatically. Set `HF_TOKEN` if a repo requires authentication. Override paths with env vars such as `SHARDED_MODEL_FIRST_SHARD_PATH` (see `test/unit/test_common.hpp`).
|
|
762
|
+
First-run downloads pull several GB, mostly from Hugging Face. Every fixture is SHA256-verified against a digest pinned in `scripts/download-unit-test-models.js`; mismatched or partial files are re-downloaded automatically. Set `HF_TOKEN` if a repo requires authentication. Fixtures pinned by `s3Path` (currently `bitnet_b1_58-large-TQ2_0.gguf`) are copied from the QVAC model registry bucket and need `MODEL_S3_BUCKET` plus AWS credentials; if either is missing or the copy fails, a local run skips the fixture with a warning and its tests skip, while `--ci` fails. Override paths with env vars such as `SHARDED_MODEL_FIRST_SHARD_PATH` or `BITNET_MODEL_PATH` (see `test/unit/test_common.hpp`).
|
|
739
763
|
|
|
740
764
|
### Fuzzing
|
|
741
765
|
|
package/addon.d.ts
CHANGED
package/index.d.ts
CHANGED
|
@@ -380,6 +380,19 @@ declare namespace LlmLlamacpp {
|
|
|
380
380
|
* maximum is 1048576 (1 TiB). Also accepted as `cache-ram-mib`.
|
|
381
381
|
*/
|
|
382
382
|
cache_ram_mib?: NumericLike;
|
|
383
|
+
/**
|
|
384
|
+
* Whether a request on a decoder-only model may start from state another
|
|
385
|
+
* conversation left: the memory a request without a `cacheKey` keeps,
|
|
386
|
+
* another key's state in a sequence or the RAM tier, or a `cacheKey` file
|
|
387
|
+
* the model has used. `'true'` (default) shares it, as llama-server's
|
|
388
|
+
* prompt cache does. `'false'` keeps every request to its own key's state,
|
|
389
|
+
* for a model serving callers who must not learn through `cacheTokens` or
|
|
390
|
+
* timing whether another conversation shares their prompt: a request
|
|
391
|
+
* without a key starts cold and is reset afterwards, a new key starts cold,
|
|
392
|
+
* and `baseCacheKey` has no effect. Also accepted as
|
|
393
|
+
* `cache-share-across-keys`.
|
|
394
|
+
*/
|
|
395
|
+
cache_share_across_keys?: "true" | "false" | "on" | "off" | "1" | "0";
|
|
383
396
|
/**
|
|
384
397
|
* Speculative decoding, as llama-server's `--spec-type`. `'draft-mtp'`
|
|
385
398
|
* drafts with the model's own multi-token-prediction (MTP / NextN) head,
|
|
@@ -437,9 +450,30 @@ declare namespace LlmLlamacpp {
|
|
|
437
450
|
rejectWhenBusy?: boolean;
|
|
438
451
|
};
|
|
439
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* One ordered piece of a message's `content`. Media parts keep their place in
|
|
455
|
+
* the turn: a single user message can interleave text and media, which a
|
|
456
|
+
* string `content` plus separate `type: "media"` messages cannot express
|
|
457
|
+
* without splitting the turn.
|
|
458
|
+
*/
|
|
459
|
+
type UserContentPart = {
|
|
460
|
+
type: "text";
|
|
461
|
+
text: string;
|
|
462
|
+
} | {
|
|
463
|
+
type: "media";
|
|
464
|
+
/**
|
|
465
|
+
* Either an absolute path to the media file (`string`) or its raw bytes
|
|
466
|
+
* (`Uint8Array`), the same two forms a `type: "media"` message accepts.
|
|
467
|
+
*/
|
|
468
|
+
content: string | Uint8Array;
|
|
469
|
+
};
|
|
440
470
|
interface UserTextMessage {
|
|
441
471
|
role: "system" | "assistant" | "user" | "tool" | "session" | string;
|
|
442
|
-
|
|
472
|
+
/**
|
|
473
|
+
* Plain text, or ordered text/media parts rendered as one turn. Media parts
|
|
474
|
+
* are only valid on user messages.
|
|
475
|
+
*/
|
|
476
|
+
content: string | UserContentPart[];
|
|
443
477
|
/**
|
|
444
478
|
* An assistant turn's reasoning, separate from its answer in `content`, as
|
|
445
479
|
* in OpenAI-compatible APIs. Optional: an answer sent back with its
|
|
@@ -554,10 +588,9 @@ declare namespace LlmLlamacpp {
|
|
|
554
588
|
* Run prefill only (cache warming): the prompt is evaluated but no tokens
|
|
555
589
|
* are generated. On a model loaded with `parallel >= 2` a prefill needs a
|
|
556
590
|
* `cacheKey` (the warmed conversation stays in its slot for the next
|
|
557
|
-
* request on that key) — a keyless prefill warms
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
* batch item.
|
|
591
|
+
* request on that key) — a keyless prefill warms state no later request
|
|
592
|
+
* can count on and is rejected with `InvalidArgument`; run keyless
|
|
593
|
+
* prefills on a `parallel: 1` model. The same rule applies per batch item.
|
|
561
594
|
*/
|
|
562
595
|
prefill?: boolean;
|
|
563
596
|
generationParams?: GenerationParams;
|
|
@@ -574,11 +607,26 @@ declare namespace LlmLlamacpp {
|
|
|
574
607
|
* otherwise be lost with turns the file lacks: when it is set aside for
|
|
575
608
|
* another key, evicted from its batch slot or from the RAM tier, or the
|
|
576
609
|
* model is reloaded or unloaded. With `ephemeral: true` each of those
|
|
577
|
-
* drops it instead, so its next request
|
|
578
|
-
*
|
|
610
|
+
* drops it instead, so its next request finds no state of its own (it
|
|
611
|
+
* still reuses any prefix it shares with what the model holds, except on
|
|
612
|
+
* encoder-decoder models). It still loads an existing file, and an
|
|
613
|
+
* explicit `saveCache(cacheKey)` still writes it.
|
|
579
614
|
* The flag belongs to the conversation's latest request.
|
|
580
615
|
*/
|
|
581
616
|
ephemeral?: boolean;
|
|
617
|
+
/**
|
|
618
|
+
* A `cacheKey` file to start from when nothing in memory serves more of
|
|
619
|
+
* the prompt, such as a system prompt and tools primed with `prefill`.
|
|
620
|
+
* It is read, never written: the request's own state goes to its
|
|
621
|
+
* `cacheKey`. A file that is missing or shares no prefix with the prompt
|
|
622
|
+
* is skipped. Once named, the file stays a source for later requests on
|
|
623
|
+
* this model, as any `cacheKey` file it used does, until files named
|
|
624
|
+
* or picked as a prompt's closest match more recently push it out (the
|
|
625
|
+
* model keeps the 256 most recent, as far as 64 MiB of memory holds
|
|
626
|
+
* their prompt records). No effect with `cache_share_across_keys:
|
|
627
|
+
* 'false'`.
|
|
628
|
+
*/
|
|
629
|
+
baseCacheKey?: string;
|
|
582
630
|
/**
|
|
583
631
|
* Admission policy when the worker pool is full. `true` rejects before
|
|
584
632
|
* submitting with an `Error` carrying `code === 'RUN_BUSY'` — branch on the
|
package/index.js
CHANGED
|
@@ -16,6 +16,7 @@ function normalizeRunOptions(runOptions) {
|
|
|
16
16
|
generationParams: undefined,
|
|
17
17
|
cacheKey: undefined,
|
|
18
18
|
ephemeral: false,
|
|
19
|
+
baseCacheKey: undefined,
|
|
19
20
|
rejectWhenBusy: undefined,
|
|
20
21
|
};
|
|
21
22
|
}
|
|
@@ -43,6 +44,9 @@ function normalizeRunOptions(runOptions) {
|
|
|
43
44
|
if (options.ephemeral !== undefined && typeof options.ephemeral !== "boolean") {
|
|
44
45
|
throw new TypeError("ephemeral must be a boolean when provided");
|
|
45
46
|
}
|
|
47
|
+
if (options.baseCacheKey !== undefined && typeof options.baseCacheKey !== "string") {
|
|
48
|
+
throw new TypeError("baseCacheKey must be a string when provided");
|
|
49
|
+
}
|
|
46
50
|
if (options.rejectWhenBusy !== undefined && typeof options.rejectWhenBusy !== "boolean") {
|
|
47
51
|
throw new TypeError("rejectWhenBusy must be a boolean when provided");
|
|
48
52
|
}
|
|
@@ -51,15 +55,28 @@ function normalizeRunOptions(runOptions) {
|
|
|
51
55
|
generationParams: normalizeGenerationParams(options.generationParams),
|
|
52
56
|
cacheKey: options.cacheKey,
|
|
53
57
|
ephemeral: options.ephemeral === true,
|
|
58
|
+
baseCacheKey: options.baseCacheKey,
|
|
54
59
|
// Left undefined when unset so admission falls back to the instance default.
|
|
55
60
|
rejectWhenBusy: options.rejectWhenBusy,
|
|
56
61
|
};
|
|
57
62
|
}
|
|
63
|
+
// Malformed parts (null, non-objects) are not byte media: they pass through
|
|
64
|
+
// untouched so the native validator rejects them with `InvalidInputFormat`.
|
|
65
|
+
function isByteMediaPart(part) {
|
|
66
|
+
const candidate = part;
|
|
67
|
+
return (candidate !== null &&
|
|
68
|
+
typeof candidate === "object" &&
|
|
69
|
+
candidate.type === "media" &&
|
|
70
|
+
candidate.content instanceof Uint8Array);
|
|
71
|
+
}
|
|
58
72
|
function promptToAddonMessages(prompt, runOptions) {
|
|
59
73
|
if (!Array.isArray(prompt)) {
|
|
60
74
|
throw new TypeError("Prompt input must be Message[]");
|
|
61
75
|
}
|
|
62
|
-
const { prefill, generationParams, cacheKey, ephemeral } = normalizeRunOptions(runOptions);
|
|
76
|
+
const { prefill, generationParams, cacheKey, ephemeral, baseCacheKey } = normalizeRunOptions(runOptions);
|
|
77
|
+
// Only ever JSON-serialized into the addon's text message: a byte-buffer
|
|
78
|
+
// media part reaches the native side path-less, bound to the hoisted buffer
|
|
79
|
+
// by position, so the wire shape is wider than the public `Message` type.
|
|
63
80
|
const textMessages = [];
|
|
64
81
|
const mediaItems = [];
|
|
65
82
|
for (const message of prompt) {
|
|
@@ -67,10 +84,26 @@ function promptToAddonMessages(prompt, runOptions) {
|
|
|
67
84
|
if (media.role === "user" && media.type === "media" && media.content instanceof Uint8Array) {
|
|
68
85
|
mediaItems.push(media.content);
|
|
69
86
|
textMessages.push({ ...media, content: "" });
|
|
87
|
+
continue;
|
|
70
88
|
}
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
// Ordered parts: hoist each media part's bytes next to the other media
|
|
90
|
+
// messages and leave a path-less part behind. The native side records the
|
|
91
|
+
// plan in marker order, so the bytes still bind to the right markers.
|
|
92
|
+
const text = message;
|
|
93
|
+
if (Array.isArray(text.content)) {
|
|
94
|
+
textMessages.push({
|
|
95
|
+
...text,
|
|
96
|
+
content: text.content.map((part) => {
|
|
97
|
+
if (isByteMediaPart(part)) {
|
|
98
|
+
mediaItems.push(part.content);
|
|
99
|
+
return { type: "media" };
|
|
100
|
+
}
|
|
101
|
+
return part;
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
73
105
|
}
|
|
106
|
+
textMessages.push(message);
|
|
74
107
|
}
|
|
75
108
|
const promptMessages = [];
|
|
76
109
|
for (const mediaData of mediaItems) {
|
|
@@ -83,6 +116,7 @@ function promptToAddonMessages(prompt, runOptions) {
|
|
|
83
116
|
generationParams,
|
|
84
117
|
cacheKey,
|
|
85
118
|
ephemeral,
|
|
119
|
+
baseCacheKey,
|
|
86
120
|
});
|
|
87
121
|
return promptMessages;
|
|
88
122
|
}
|
|
@@ -257,6 +291,15 @@ function sanitizePromptForLog(prompt) {
|
|
|
257
291
|
if (media && media.type === "media" && media.content instanceof Uint8Array) {
|
|
258
292
|
return { ...media, content: `[Uint8Array byteLength=${media.content.byteLength}]` };
|
|
259
293
|
}
|
|
294
|
+
const text = msg;
|
|
295
|
+
if (Array.isArray(text?.content)) {
|
|
296
|
+
return {
|
|
297
|
+
...text,
|
|
298
|
+
content: text.content.map((part) => isByteMediaPart(part)
|
|
299
|
+
? { ...part, content: `[Uint8Array byteLength=${part.content.byteLength}]` }
|
|
300
|
+
: part),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
260
303
|
return msg;
|
|
261
304
|
});
|
|
262
305
|
}
|
|
@@ -863,9 +906,20 @@ const LlmLlamacpp = class LlmLlamacpp {
|
|
|
863
906
|
if (text !== undefined)
|
|
864
907
|
params.text = text;
|
|
865
908
|
// Media bytes are dropped rather than serialized: the native side rejects
|
|
866
|
-
// any media message by its type.
|
|
909
|
+
// any media message or ordered media part by its type.
|
|
867
910
|
if (history !== undefined) {
|
|
868
|
-
params.history = JSON.stringify(history.map((message) =>
|
|
911
|
+
params.history = JSON.stringify(history.map((message) => {
|
|
912
|
+
if (message?.type === "media")
|
|
913
|
+
return { ...message, content: "" };
|
|
914
|
+
const textMessage = message;
|
|
915
|
+
if (Array.isArray(textMessage?.content)) {
|
|
916
|
+
return {
|
|
917
|
+
...message,
|
|
918
|
+
content: textMessage.content.map((part) => isByteMediaPart(part) ? { type: "media" } : part),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
return message;
|
|
922
|
+
}));
|
|
869
923
|
}
|
|
870
924
|
if (tools !== undefined)
|
|
871
925
|
params.tools = JSON.stringify(tools);
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|