wontopos 2.2.7 → 2.2.8
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 +32 -4
- package/dist/wontopos.d.ts +20 -2
- package/dist/wontopos.js +132 -44
- package/package.json +2 -9
package/README.md
CHANGED
|
@@ -55,9 +55,26 @@ const hits = await mem.search("...the conversation so far...", "alice", 10,
|
|
|
55
55
|
{ cache_control: { ttl: "5m" } }); // or "1h"
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
## Reliability
|
|
59
|
+
|
|
60
|
+
Built in, no configuration needed:
|
|
61
|
+
|
|
62
|
+
- **Automatic retries** — 429 / 502 / 503 and connection errors retry twice with
|
|
63
|
+
exponential backoff + jitter, honoring the server's `Retry-After`. Tune with
|
|
64
|
+
`new Client({ maxRetries })`; `0` disables.
|
|
65
|
+
- **Redirects refused** — the API key never follows a 3xx to another host.
|
|
66
|
+
- **Timeouts** — 30s per attempt by default (`timeoutMs`).
|
|
67
|
+
- **Key never in logs** — `JSON.stringify(client)` and Node's `console.log(client)`
|
|
68
|
+
print a masked key.
|
|
69
|
+
- **Wipe guard** — `delete()` without a `memoryId` throws instead of silently
|
|
70
|
+
meaning "delete everything"; wiping a store is only ever the explicit
|
|
71
|
+
`deleteAll(userId)` / `deleteStore(userId)`.
|
|
58
72
|
|
|
59
73
|
## Errors
|
|
60
74
|
|
|
75
|
+
Any non-2xx response throws `WosError` with `status`, `message`, and — when the
|
|
76
|
+
server sent one — `requestId` (include it when contacting support).
|
|
77
|
+
|
|
61
78
|
```ts
|
|
62
79
|
import { Client, WosError } from "wontopos";
|
|
63
80
|
|
|
@@ -66,8 +83,8 @@ try {
|
|
|
66
83
|
} catch (e) {
|
|
67
84
|
if (e instanceof WosError) {
|
|
68
85
|
if (e.status === 401) console.error("API key invalid or revoked");
|
|
69
|
-
else if (e.status === 429) console.error("Rate limited");
|
|
70
|
-
else console.error(e.status, e.message);
|
|
86
|
+
else if (e.status === 429) console.error("Rate limited"); // already retried twice by then
|
|
87
|
+
else console.error(e.status, e.message, e.requestId);
|
|
71
88
|
}
|
|
72
89
|
}
|
|
73
90
|
```
|
|
@@ -81,7 +98,18 @@ const mem = new Client({ apiKey: "...", baseUrl: "https://wos.your-host.com" });
|
|
|
81
98
|
## Links
|
|
82
99
|
|
|
83
100
|
- Homepage: <https://wontopos.com>
|
|
84
|
-
- API reference: <https://wontopos.com/why> (Developers tab)
|
|
85
|
-
|
|
101
|
+
- API reference: <https://wontopos.com/en/why> (Developers tab)
|
|
102
|
+
|
|
103
|
+
## Changelog
|
|
104
|
+
|
|
105
|
+
- **2.2.8** — reliability + hardening: automatic retries (429/502/503 + connection
|
|
106
|
+
errors, backoff + jitter, honors `Retry-After`), redirects refused, key masked in
|
|
107
|
+
`toJSON`/inspect, `WosError.requestId`, guard against `delete()` without
|
|
108
|
+
`memoryId`, plain-HTTP warning, `User-Agent`.
|
|
109
|
+
- **2.2.7** — speakers: `addSpeaker` / `listSpeakers` / `removeSpeaker`.
|
|
110
|
+
- **2.2.6** — docs: `userId` = store, clarified everywhere.
|
|
111
|
+
- **2.2.5** — docs: speakers + recall caching sections.
|
|
112
|
+
- **2.2.4** — Python/TypeScript/Rust converge on one version; all three now
|
|
113
|
+
release in lockstep (same version, same surface). Patch releases are additive.
|
|
86
114
|
|
|
87
115
|
License: MIT.
|
package/dist/wontopos.d.ts
CHANGED
|
@@ -21,15 +21,24 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Pure semantic retrieval — no keyword/BM25 matching, so it works the SAME in every
|
|
23
23
|
* language. Storing and searching call no LLM; you only pay for embeddings.
|
|
24
|
+
*
|
|
25
|
+
* Reliability: every call retries transient failures (429 / 502 / 503 and
|
|
26
|
+
* connection errors) with exponential backoff + jitter, honoring `Retry-After`.
|
|
27
|
+
* Redirects are refused so the API key can never follow one to another host.
|
|
28
|
+
* Tune with `maxRetries` (0 disables) and `timeoutMs`.
|
|
24
29
|
*/
|
|
25
30
|
export declare class WosError extends Error {
|
|
26
31
|
readonly status: number;
|
|
27
|
-
|
|
32
|
+
/** The server's id for the request when it sent one — include it when contacting support. */
|
|
33
|
+
readonly requestId?: string;
|
|
34
|
+
constructor(status: number, message: string, requestId?: string);
|
|
28
35
|
}
|
|
29
36
|
export interface Memory {
|
|
30
37
|
id?: string;
|
|
31
38
|
content?: string;
|
|
32
39
|
score?: number;
|
|
40
|
+
/** WHO said it: "me" (the assistant) or a registered person's name. Absent = untagged. */
|
|
41
|
+
speaker?: string;
|
|
33
42
|
[key: string]: unknown;
|
|
34
43
|
}
|
|
35
44
|
export interface RecallResult {
|
|
@@ -58,13 +67,16 @@ export interface ClientOptions {
|
|
|
58
67
|
apiKey: string;
|
|
59
68
|
/** API base URL (defaults to the hosted service). */
|
|
60
69
|
baseUrl?: string;
|
|
61
|
-
/** Per-request timeout in ms (default 30000). */
|
|
70
|
+
/** Per-request timeout in ms, applied to each retry attempt (default 30000). */
|
|
62
71
|
timeoutMs?: number;
|
|
63
72
|
/** Default model for every call (sent as `X-WOS-Model`). See `listModels()`. */
|
|
64
73
|
model?: string;
|
|
65
74
|
/** Default store for every call. Override per call by passing `userId`. Defaults
|
|
66
75
|
* to the account's built-in `default` store. */
|
|
67
76
|
userId?: string;
|
|
77
|
+
/** How many times to retry transient failures (429/502/503 and connection
|
|
78
|
+
* errors) before throwing. 0 disables retries (default 2). */
|
|
79
|
+
maxRetries?: number;
|
|
68
80
|
}
|
|
69
81
|
/** Wontopos memory client. Store and recall memories, isolated per `userId`. */
|
|
70
82
|
export declare class Client {
|
|
@@ -73,11 +85,15 @@ export declare class Client {
|
|
|
73
85
|
private readonly timeoutMs;
|
|
74
86
|
private readonly apiKey;
|
|
75
87
|
private readonly model;
|
|
88
|
+
private readonly maxRetries;
|
|
76
89
|
/** The store every call uses unless one passes `userId`. */
|
|
77
90
|
private readonly defaultUser;
|
|
78
91
|
constructor(opts: ClientOptions);
|
|
92
|
+
/** Never expose the key: `JSON.stringify(client)` gets the redacted view. */
|
|
93
|
+
toJSON(): Record<string, string>;
|
|
79
94
|
/** Resolve a call's store: the explicit userId, else the client default. */
|
|
80
95
|
private uid;
|
|
96
|
+
private clone;
|
|
81
97
|
/**
|
|
82
98
|
* A client that uses `model` (sent as `X-WOS-Model`). Use it to set the default
|
|
83
99
|
* (`new Client({ apiKey, model })`) or to override one call
|
|
@@ -135,6 +151,8 @@ export declare class Client {
|
|
|
135
151
|
* this is destructive, so it never falls back to the default store. */
|
|
136
152
|
deleteAll(userId: string): Promise<any>;
|
|
137
153
|
private post;
|
|
154
|
+
/** Seconds→ms to sleep before retry `attempt` (0-based). Honors Retry-After. */
|
|
155
|
+
private backoffMs;
|
|
138
156
|
private request;
|
|
139
157
|
}
|
|
140
158
|
/** Back-compat alias: older code used `WME`. */
|
package/dist/wontopos.js
CHANGED
|
@@ -21,15 +21,32 @@
|
|
|
21
21
|
*
|
|
22
22
|
* Pure semantic retrieval — no keyword/BM25 matching, so it works the SAME in every
|
|
23
23
|
* language. Storing and searching call no LLM; you only pay for embeddings.
|
|
24
|
+
*
|
|
25
|
+
* Reliability: every call retries transient failures (429 / 502 / 503 and
|
|
26
|
+
* connection errors) with exponential backoff + jitter, honoring `Retry-After`.
|
|
27
|
+
* Redirects are refused so the API key can never follow one to another host.
|
|
28
|
+
* Tune with `maxRetries` (0 disables) and `timeoutMs`.
|
|
24
29
|
*/
|
|
30
|
+
const VERSION = "2.2.8";
|
|
31
|
+
const USER_AGENT = `wontopos-node/${VERSION}`;
|
|
25
32
|
const DEFAULT_BASE_URL = "https://api.wontopos.com";
|
|
26
33
|
const DEFAULT_MODEL = "tablet-1";
|
|
34
|
+
/** Statuses where the request was NOT processed, so retrying is always safe. */
|
|
35
|
+
const RETRY_STATUSES = new Set([429, 502, 503]);
|
|
36
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
37
|
+
/** `wos-abc...wxyz` — enough to tell keys apart, never enough to use. */
|
|
38
|
+
function maskKey(key) {
|
|
39
|
+
return key.length > 12 ? `${key.slice(0, 4)}...${key.slice(-4)}` : "***";
|
|
40
|
+
}
|
|
27
41
|
export class WosError extends Error {
|
|
28
42
|
status;
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
/** The server's id for the request when it sent one — include it when contacting support. */
|
|
44
|
+
requestId;
|
|
45
|
+
constructor(status, message, requestId) {
|
|
46
|
+
super(`[${status}] ${message}${requestId ? ` (request_id: ${requestId})` : ""}`);
|
|
31
47
|
this.name = "WosError";
|
|
32
48
|
this.status = status;
|
|
49
|
+
this.requestId = requestId;
|
|
33
50
|
}
|
|
34
51
|
}
|
|
35
52
|
const DEFAULT_USER = "default";
|
|
@@ -40,6 +57,7 @@ export class Client {
|
|
|
40
57
|
timeoutMs;
|
|
41
58
|
apiKey;
|
|
42
59
|
model;
|
|
60
|
+
maxRetries;
|
|
43
61
|
/** The store every call uses unless one passes `userId`. */
|
|
44
62
|
defaultUser;
|
|
45
63
|
constructor(opts) {
|
|
@@ -50,25 +68,57 @@ export class Client {
|
|
|
50
68
|
this.timeoutMs = opts.timeoutMs ?? 30_000;
|
|
51
69
|
this.model = opts.model ?? DEFAULT_MODEL;
|
|
52
70
|
this.defaultUser = opts.userId || DEFAULT_USER;
|
|
53
|
-
this.
|
|
71
|
+
this.maxRetries = Math.max(0, opts.maxRetries ?? 2);
|
|
72
|
+
this.headers = {
|
|
73
|
+
"X-API-Key": opts.apiKey,
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
// Browsers silently drop User-Agent (forbidden header) — harmless.
|
|
76
|
+
"User-Agent": USER_AGENT,
|
|
77
|
+
};
|
|
54
78
|
if (this.model)
|
|
55
79
|
this.headers["X-WOS-Model"] = this.model;
|
|
80
|
+
// An API key on plain HTTP travels readable by anyone on the path. Loopback
|
|
81
|
+
// is fine (local dev / self-hosted on the same box); anything else gets a
|
|
82
|
+
// warning, not an error, so private-network gateways keep working.
|
|
83
|
+
const host = this.base.replace(/^https?:\/\//, "").split(/[/:]/)[0].toLowerCase();
|
|
84
|
+
if (this.base.startsWith("http://") && !LOOPBACK_HOSTS.has(host)) {
|
|
85
|
+
console.warn("wontopos: baseUrl uses plain HTTP on a non-local host, so the API key travels unencrypted. Use https://.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Never expose the key: `JSON.stringify(client)` gets the redacted view. */
|
|
89
|
+
toJSON() {
|
|
90
|
+
return { baseUrl: this.base, model: this.model, userId: this.defaultUser, apiKey: maskKey(this.apiKey) };
|
|
91
|
+
}
|
|
92
|
+
/** Never expose the key: Node's `console.log(client)` gets the redacted view. */
|
|
93
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
94
|
+
return `Client(${this.base}, model=${this.model}, userId=${this.defaultUser}, apiKey=${maskKey(this.apiKey)})`;
|
|
56
95
|
}
|
|
57
96
|
/** Resolve a call's store: the explicit userId, else the client default. */
|
|
58
97
|
uid(userId) {
|
|
59
98
|
return userId || this.defaultUser;
|
|
60
99
|
}
|
|
100
|
+
clone(overrides) {
|
|
101
|
+
return new Client({
|
|
102
|
+
apiKey: this.apiKey,
|
|
103
|
+
baseUrl: this.base,
|
|
104
|
+
timeoutMs: this.timeoutMs,
|
|
105
|
+
model: this.model,
|
|
106
|
+
userId: this.defaultUser,
|
|
107
|
+
maxRetries: this.maxRetries,
|
|
108
|
+
...overrides,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
61
111
|
/**
|
|
62
112
|
* A client that uses `model` (sent as `X-WOS-Model`). Use it to set the default
|
|
63
113
|
* (`new Client({ apiKey, model })`) or to override one call
|
|
64
114
|
* (`client.withModel("tablet-1").recall(...)`).
|
|
65
115
|
*/
|
|
66
116
|
withModel(model) {
|
|
67
|
-
return
|
|
117
|
+
return this.clone({ model });
|
|
68
118
|
}
|
|
69
119
|
/** A client bound to `userId` as its default store (everything else kept). */
|
|
70
120
|
withUser(userId) {
|
|
71
|
-
return
|
|
121
|
+
return this.clone({ userId });
|
|
72
122
|
}
|
|
73
123
|
// ----- write -----
|
|
74
124
|
// `userId` is optional in every call below - omit it to use the client's default
|
|
@@ -123,10 +173,7 @@ export class Client {
|
|
|
123
173
|
// ----- models -----
|
|
124
174
|
/** Available models: `[{ id, name, available, memory }, ...]`. Needs no API key. */
|
|
125
175
|
async listModels() {
|
|
126
|
-
const
|
|
127
|
-
if (!res.ok)
|
|
128
|
-
throw new WosError(res.status, await res.text());
|
|
129
|
-
const data = await res.json();
|
|
176
|
+
const data = await this.request("GET", "/api/v1/models");
|
|
130
177
|
return (data.models ?? []);
|
|
131
178
|
}
|
|
132
179
|
// ----- stores -----
|
|
@@ -144,6 +191,8 @@ export class Client {
|
|
|
144
191
|
}
|
|
145
192
|
/** Delete a store and ALL its memories. Returns `{ user_id, status }`. */
|
|
146
193
|
deleteStore(userId) {
|
|
194
|
+
if (!userId)
|
|
195
|
+
throw new Error("userId is required — deleteStore never falls back to the default store.");
|
|
147
196
|
return this.request("DELETE", "/api/v1/memory/collection", { user_id: userId });
|
|
148
197
|
}
|
|
149
198
|
// ----- speakers (who said it) -----
|
|
@@ -164,58 +213,97 @@ export class Client {
|
|
|
164
213
|
// ----- delete -----
|
|
165
214
|
/** Delete a single memory by id. `userId` optional (uses the default store). */
|
|
166
215
|
delete(userId, memoryId) {
|
|
216
|
+
// Guard: without a memory_id the API's forget endpoint means "delete the
|
|
217
|
+
// whole store". An undefined/"" slipping in here must never become a wipe.
|
|
218
|
+
if (!memoryId || typeof memoryId !== "string") {
|
|
219
|
+
throw new Error("memoryId is required. To delete every memory in a store, call deleteAll(userId) explicitly.");
|
|
220
|
+
}
|
|
167
221
|
return this.post("/api/v1/memory/forget", { user_id: this.uid(userId), memory_id: memoryId });
|
|
168
222
|
}
|
|
169
223
|
/** Delete ALL memories for a store (GDPR erase). `userId` is required on purpose -
|
|
170
224
|
* this is destructive, so it never falls back to the default store. */
|
|
171
225
|
deleteAll(userId) {
|
|
226
|
+
if (!userId)
|
|
227
|
+
throw new Error("userId is required for deleteAll — it never falls back to the default store.");
|
|
172
228
|
return this.post("/api/v1/memory/forget", { user_id: userId });
|
|
173
229
|
}
|
|
174
230
|
// ----- internal -----
|
|
175
231
|
post(path, body) {
|
|
176
232
|
return this.request("POST", path, body);
|
|
177
233
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
method,
|
|
185
|
-
headers: this.headers,
|
|
186
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
187
|
-
signal: ctrl.signal,
|
|
188
|
-
});
|
|
234
|
+
/** Seconds→ms to sleep before retry `attempt` (0-based). Honors Retry-After. */
|
|
235
|
+
backoffMs(attempt, retryAfter) {
|
|
236
|
+
if (retryAfter) {
|
|
237
|
+
const s = Number(retryAfter);
|
|
238
|
+
if (Number.isFinite(s) && s >= 0)
|
|
239
|
+
return Math.min(30_000, s * 1000);
|
|
189
240
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// {"type":"error","error":{"type":...,"message":...,"request_id":...}}
|
|
199
|
-
// or simple {"error":"reason"}. Fall back to raw text.
|
|
200
|
-
let msg = await res.text();
|
|
241
|
+
return Math.min(8_000, 500 * 2 ** attempt) + Math.random() * 250;
|
|
242
|
+
}
|
|
243
|
+
async request(method, path, body) {
|
|
244
|
+
const attempts = this.maxRetries + 1;
|
|
245
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
246
|
+
const ctrl = new AbortController();
|
|
247
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
248
|
+
let res;
|
|
201
249
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
250
|
+
res = await fetch(`${this.base}${path}`, {
|
|
251
|
+
method,
|
|
252
|
+
headers: this.headers,
|
|
253
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
254
|
+
signal: ctrl.signal,
|
|
255
|
+
// Never follow a redirect: fetch would forward the API key to
|
|
256
|
+
// wherever a 3xx points. The API never legitimately redirects.
|
|
257
|
+
redirect: "manual",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
// Timeouts are ambiguous (the write may have landed) — don't retry those.
|
|
263
|
+
const timedOut = e?.name === "AbortError" || ctrl.signal.aborted;
|
|
264
|
+
if (!timedOut && attempt + 1 < attempts) {
|
|
265
|
+
await new Promise((r) => setTimeout(r, this.backoffMs(attempt)));
|
|
266
|
+
continue;
|
|
211
267
|
}
|
|
268
|
+
throw new WosError(0, timedOut ? `request timed out after ${this.timeoutMs}ms` : `network error: ${e?.message ?? e}`);
|
|
212
269
|
}
|
|
213
|
-
|
|
214
|
-
|
|
270
|
+
clearTimeout(timer);
|
|
271
|
+
if (RETRY_STATUSES.has(res.status) && attempt + 1 < attempts) {
|
|
272
|
+
await new Promise((r) => setTimeout(r, this.backoffMs(attempt, res.headers.get("Retry-After"))));
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if ((res.status >= 300 && res.status < 400) || res.type === "opaqueredirect") {
|
|
276
|
+
throw new WosError(res.status, "unexpected redirect — refused (the API key never follows a redirect). Check baseUrl: exact host, https://.");
|
|
277
|
+
}
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
// Server returns either Anthropic-style envelope
|
|
280
|
+
// {"type":"error","error":{"type":...,"message":...,"request_id":...}}
|
|
281
|
+
// or simple {"error":"reason"}. Fall back to raw text.
|
|
282
|
+
let msg = await res.text();
|
|
283
|
+
let requestId;
|
|
284
|
+
try {
|
|
285
|
+
const data = JSON.parse(msg);
|
|
286
|
+
if (data && typeof data === "object") {
|
|
287
|
+
const err = data.error;
|
|
288
|
+
if (err && typeof err === "object") {
|
|
289
|
+
msg = err.message ?? err.type ?? msg;
|
|
290
|
+
if (typeof err.request_id === "string")
|
|
291
|
+
requestId = err.request_id;
|
|
292
|
+
}
|
|
293
|
+
else if (typeof err === "string")
|
|
294
|
+
msg = err;
|
|
295
|
+
else if (typeof data.message === "string")
|
|
296
|
+
msg = data.message;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
/* keep raw text */
|
|
301
|
+
}
|
|
302
|
+
throw new WosError(res.status, msg, requestId);
|
|
215
303
|
}
|
|
216
|
-
|
|
304
|
+
return res.json();
|
|
217
305
|
}
|
|
218
|
-
|
|
306
|
+
throw new WosError(0, "retries exhausted"); // unreachable; keeps the compiler happy
|
|
219
307
|
}
|
|
220
308
|
}
|
|
221
309
|
/** Back-compat alias: older code used `WME`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wontopos",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.8",
|
|
4
4
|
"description": "Wontopos — long-term memory for AI agents. Pure semantic retrieval, identical recall in every language, ~100× lower LLM bill.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"memory",
|
|
@@ -14,14 +14,6 @@
|
|
|
14
14
|
"context"
|
|
15
15
|
],
|
|
16
16
|
"homepage": "https://wontopos.com",
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/Irina1920/Wos_API.git",
|
|
20
|
-
"directory": "sdk/typescript"
|
|
21
|
-
},
|
|
22
|
-
"bugs": {
|
|
23
|
-
"url": "https://github.com/Irina1920/Wos_API/issues"
|
|
24
|
-
},
|
|
25
17
|
"license": "MIT",
|
|
26
18
|
"author": "Wontopos",
|
|
27
19
|
"type": "module",
|
|
@@ -45,6 +37,7 @@
|
|
|
45
37
|
},
|
|
46
38
|
"scripts": {
|
|
47
39
|
"build": "tsc",
|
|
40
|
+
"test": "npm run build && node --test test/client.test.mjs",
|
|
48
41
|
"prepublishOnly": "npm run build"
|
|
49
42
|
},
|
|
50
43
|
"devDependencies": {
|